@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,694 @@
1
+ # Search Module
2
+
3
+ The search module provides unified search capabilities across all entities in Open Mercato, supporting multiple search strategies including Meilisearch (full-text) and vector embeddings (semantic search).
4
+
5
+ ## Features
6
+
7
+ - **Multi-strategy search**: Combines full-text search (Meilisearch), vector-based semantic search, and token matching
8
+ - **Automatic indexing**: Subscribes to entity events for real-time index updates
9
+ - **Queue-based processing**: Supports async batch processing via Redis/BullMQ for high-volume indexing
10
+ - **Configurable embeddings**: Supports OpenAI, Ollama, and other embedding providers
11
+ - **Tenant-scoped**: All indexes are scoped by tenant and optionally by organization
12
+ - **Admin-configurable**: Global search (Cmd+K) strategies can be configured per-tenant
13
+
14
+ ## Global Search Settings
15
+
16
+ The global search dialog (Cmd+K) can be configured by administrators to control which search strategies are used. This configuration is stored per-tenant.
17
+
18
+ ### Admin Configuration
19
+
20
+ Navigate to **Settings > Search** to configure which search methods are enabled for Cmd+K:
21
+
22
+ - **Full-Text Search**: Fast, typo-tolerant search powered by Meilisearch
23
+ - **Semantic Search (AI)**: AI-powered search that understands meaning (requires embedding provider)
24
+ - **Keyword Search**: Exact word matching in the database
25
+
26
+ ### API Endpoint
27
+
28
+ **`GET /api/search/settings/global-search`**
29
+
30
+ Returns the currently enabled strategies for global search.
31
+
32
+ **Response:**
33
+ ```json
34
+ {
35
+ "enabledStrategies": ["fulltext", "vector", "tokens"]
36
+ }
37
+ ```
38
+
39
+ **`POST /api/search/settings/global-search`**
40
+
41
+ Updates the enabled strategies.
42
+
43
+ **Request:**
44
+ ```json
45
+ {
46
+ "enabledStrategies": ["fulltext", "vector"]
47
+ }
48
+ ```
49
+
50
+ **Permissions:**
51
+ - GET requires `search.view`
52
+ - POST requires `search.manage`
53
+
54
+ ## Programmatic Integration (DI)
55
+
56
+ Other modules can use the search functionality by resolving services from the DI container.
57
+
58
+ ### SearchService
59
+
60
+ The primary service for executing searches and managing indexes:
61
+
62
+ ```typescript
63
+ import type { SearchService } from '@open-mercato/search'
64
+
65
+ // Resolve from DI container
66
+ const searchService = container.resolve('searchService') as SearchService
67
+
68
+ // Execute a search
69
+ const results = await searchService.search('john doe', {
70
+ tenantId: 'tenant-123',
71
+ organizationId: 'org-456', // optional
72
+ limit: 20,
73
+ strategies: ['fulltext', 'vector'], // optional - defaults to all available
74
+ })
75
+
76
+ // Index a record
77
+ await searchService.index({
78
+ entityId: 'customers:customer_person_profile',
79
+ recordId: 'rec-123',
80
+ tenantId: 'tenant-123',
81
+ organizationId: 'org-456',
82
+ fields: { name: 'John Doe', email: 'john@example.com' },
83
+ presenter: { title: 'John Doe', subtitle: 'Customer' },
84
+ url: '/backend/customers/people/rec-123',
85
+ })
86
+
87
+ // Bulk index multiple records
88
+ await searchService.bulkIndex([record1, record2, record3])
89
+
90
+ // Delete from all indexes
91
+ await searchService.delete('customers:customer_person_profile', 'rec-123', 'tenant-123')
92
+
93
+ // Purge all records for an entity type
94
+ await searchService.purge('customers:customer_person_profile', 'tenant-123')
95
+
96
+ // Check strategy availability
97
+ const isAvailable = await searchService.isStrategyAvailable('fulltext')
98
+ ```
99
+
100
+ ### SearchIndexer (Higher-Level API)
101
+
102
+ For config-aware indexing with automatic presenter/URL resolution:
103
+
104
+ ```typescript
105
+ import type { SearchIndexer } from '@open-mercato/search'
106
+
107
+ const searchIndexer = container.resolve('searchIndexer') as SearchIndexer
108
+
109
+ // Index with automatic config-based formatting
110
+ await searchIndexer.indexRecord({
111
+ entityId: 'customers:customer_person_profile',
112
+ recordId: 'rec-123',
113
+ tenantId: 'tenant-123',
114
+ organizationId: 'org-456',
115
+ record: { id: 'rec-123', name: 'John Doe', email: 'john@example.com' },
116
+ customFields: { priority: 'high' },
117
+ })
118
+
119
+ // Check if entity is enabled for search
120
+ if (searchIndexer.isEntityEnabled('customers:customer_person_profile')) {
121
+ // Entity is configured for indexing
122
+ }
123
+
124
+ // List all search-enabled entities
125
+ const entities = searchIndexer.listEnabledEntities()
126
+
127
+ // Reindex to Meilisearch with queue support
128
+ const result = await searchIndexer.reindexEntityToMeilisearch({
129
+ entityId: 'customers:customer_person_profile',
130
+ tenantId: 'tenant-123',
131
+ organizationId: 'org-456',
132
+ recreateIndex: true,
133
+ useQueue: true, // Use async queue if available
134
+ })
135
+ ```
136
+
137
+ ### SearchService Methods
138
+
139
+ | Method | Description |
140
+ |--------|-------------|
141
+ | `search(query, options)` | Execute search across strategies |
142
+ | `index(record)` | Index a single record |
143
+ | `bulkIndex(records)` | Bulk index multiple records |
144
+ | `delete(entityId, recordId, tenantId)` | Delete from all strategies |
145
+ | `purge(entityId, tenantId)` | Purge all records for entity type |
146
+ | `registerStrategy(strategy)` | Add custom strategy at runtime |
147
+ | `unregisterStrategy(strategyId)` | Remove a strategy |
148
+ | `getRegisteredStrategies()` | List registered strategy IDs |
149
+ | `getStrategy(strategyId)` | Get specific strategy instance |
150
+ | `isStrategyAvailable(strategyId)` | Check strategy availability |
151
+
152
+ ### SearchIndexer Methods
153
+
154
+ | Method | Description |
155
+ |--------|-------------|
156
+ | `indexRecord(params)` | Index with config-based formatting |
157
+ | `deleteRecord(params)` | Delete with config handling |
158
+ | `bulkIndexRecords(params[])` | Bulk index with formatting |
159
+ | `purgeEntity(params)` | Purge entity type from indexes |
160
+ | `reindexEntityToMeilisearch(params)` | Reindex single entity |
161
+ | `reindexAllToMeilisearch(params)` | Reindex all entities |
162
+ | `getEntityConfig(entityId)` | Get entity search configuration |
163
+ | `isEntityEnabled(entityId)` | Check if entity is search-enabled |
164
+ | `listEnabledEntities()` | List all enabled entities |
165
+
166
+ ## REST API
167
+
168
+ ### Search Endpoint
169
+
170
+ **`GET /api/search`**
171
+
172
+ Execute a search query via HTTP.
173
+
174
+ **Query Parameters:**
175
+
176
+ | Parameter | Type | Required | Description |
177
+ |-----------|------|----------|-------------|
178
+ | `q` | string | Yes | Search query |
179
+ | `limit` | number | No | Max results (default: 50, max: 100) |
180
+ | `strategies` | string | No | Comma-separated strategy IDs (e.g., `fulltext,vector`) |
181
+
182
+ **Headers:**
183
+ - Requires authentication (session cookie or bearer token)
184
+ - Requires `search.view` feature permission
185
+
186
+ **Example Request:**
187
+
188
+ ```bash
189
+ curl -X GET "https://your-app.com/api/search?q=john%20doe&limit=20" \
190
+ -H "Authorization: Bearer <token>"
191
+ ```
192
+
193
+ **Response:**
194
+
195
+ ```json
196
+ {
197
+ "results": [
198
+ {
199
+ "entityId": "customers:customer_person_profile",
200
+ "recordId": "rec-123",
201
+ "score": 0.95,
202
+ "source": "fulltext",
203
+ "presenter": {
204
+ "title": "John Doe",
205
+ "subtitle": "Customer",
206
+ "icon": "user"
207
+ },
208
+ "url": "/backend/customers/people/rec-123",
209
+ "links": [
210
+ { "label": "View", "url": "/backend/customers/people/rec-123" }
211
+ ]
212
+ }
213
+ ],
214
+ "strategiesUsed": ["fulltext", "vector"],
215
+ "timing": 45,
216
+ "query": "john doe",
217
+ "limit": 20
218
+ }
219
+ ```
220
+
221
+ **Error Responses:**
222
+
223
+ | Status | Description |
224
+ |--------|-------------|
225
+ | 400 | Missing query parameter |
226
+ | 401 | Unauthorized |
227
+ | 503 | Search service unavailable |
228
+
229
+ ### Other API Endpoints
230
+
231
+ | Endpoint | Method | Description |
232
+ |----------|--------|-------------|
233
+ | `/api/search/settings/global-search` | GET | Get global search strategy configuration |
234
+ | `/api/search/settings/global-search` | POST | Update global search strategy configuration |
235
+ | `/api/search/reindex` | POST | Trigger full-text search reindex |
236
+ | `/api/search/embeddings/reindex` | POST | Trigger vector embeddings reindex |
237
+ | `/api/search/embeddings/status` | GET | Get vector indexing status |
238
+ | `/api/search/embeddings/config` | POST | Update embedding configuration |
239
+ | `/api/search/index` | GET | List indexed entries |
240
+ | `/api/search/index` | DELETE | Purge vector index |
241
+
242
+ ## Types
243
+
244
+ ### SearchOptions
245
+
246
+ ```typescript
247
+ interface SearchOptions {
248
+ tenantId: string
249
+ organizationId?: string | null
250
+ limit?: number
251
+ strategies?: SearchStrategyId[] // 'fulltext' | 'vector' | 'tokens'
252
+ entityTypes?: string[] // Filter by entity types
253
+ }
254
+ ```
255
+
256
+ ### SearchResult
257
+
258
+ ```typescript
259
+ interface SearchResult {
260
+ entityId: string // e.g., 'customers:customer_person_profile'
261
+ recordId: string // Primary key of the record
262
+ score: number // Relevance score (0-1)
263
+ source: SearchStrategyId // Which strategy returned this result
264
+ presenter?: {
265
+ title: string // Display title
266
+ subtitle?: string // Secondary text
267
+ icon?: string // Icon identifier
268
+ }
269
+ url?: string // Link to the record
270
+ links?: Array<{
271
+ label: string
272
+ url: string
273
+ }>
274
+ }
275
+ ```
276
+
277
+ ### IndexableRecord
278
+
279
+ ```typescript
280
+ interface IndexableRecord {
281
+ entityId: string
282
+ recordId: string
283
+ tenantId: string
284
+ organizationId?: string | null
285
+ fields: Record<string, unknown> // Searchable field values
286
+ presenter?: {
287
+ title: string
288
+ subtitle?: string
289
+ icon?: string
290
+ }
291
+ url?: string
292
+ links?: Array<{ label: string; url: string }>
293
+ }
294
+ ```
295
+
296
+ ## CLI Commands
297
+
298
+ The search module exposes CLI commands via `yarn mercato search <command>`.
299
+
300
+ ### Status
301
+
302
+ Show search module status and available strategies:
303
+
304
+ ```bash
305
+ yarn mercato search status
306
+ ```
307
+
308
+ ### Query
309
+
310
+ Execute a search query:
311
+
312
+ ```bash
313
+ yarn mercato search query --query "search terms" --tenant <id> [options]
314
+ ```
315
+
316
+ Options:
317
+ - `--query, -q` - Search query (required)
318
+ - `--tenant` - Tenant ID (required)
319
+ - `--org` - Organization ID (optional)
320
+ - `--entity` - Entity types to search (comma-separated)
321
+ - `--strategy` - Strategies to use (comma-separated: fulltext, vector, tokens)
322
+ - `--limit` - Max results (default: 20)
323
+
324
+ ### Index
325
+
326
+ Index a specific record:
327
+
328
+ ```bash
329
+ yarn mercato search index --entity <entityId> --record <recordId> --tenant <tenantId>
330
+ ```
331
+
332
+ Options:
333
+ - `--entity` - Entity ID (e.g., `customers:customer_person_profile`)
334
+ - `--record` - Record ID
335
+ - `--tenant` - Tenant ID
336
+ - `--org` - Organization ID (optional)
337
+
338
+ ### Reindex
339
+
340
+ Reindex vector embeddings for entities:
341
+
342
+ ```bash
343
+ yarn mercato search reindex --tenant <id> [options]
344
+ ```
345
+
346
+ Options:
347
+ - `--tenant <id>` - Tenant scope (required for purge & coverage)
348
+ - `--org <id>` - Organization scope (requires tenant)
349
+ - `--entity <module:entity>` - Reindex a single entity (defaults to all enabled entities)
350
+ - `--partitions <n>` - Number of partitions to process in parallel
351
+ - `--partition <idx>` - Restrict to a specific partition index
352
+ - `--batch <n>` - Override batch size per chunk
353
+ - `--force` - Force reindex even if another job is running
354
+ - `--purgeFirst` - Purge vector rows before reindexing
355
+ - `--skipPurge` - Explicitly skip purging vector rows
356
+ - `--skipResetCoverage` - Keep existing coverage snapshots
357
+
358
+ Use `yarn mercato search reindex-help` for detailed options.
359
+
360
+ ### Test Meilisearch
361
+
362
+ Test the Meilisearch connection:
363
+
364
+ ```bash
365
+ yarn mercato search test-meilisearch
366
+ ```
367
+
368
+ ### Worker
369
+
370
+ Start a queue worker for processing search indexing jobs:
371
+
372
+ ```bash
373
+ yarn mercato search worker <queue-name> [options]
374
+ ```
375
+
376
+ Available queues:
377
+ - `vector-indexing` - Process vector embedding indexing jobs
378
+ - `fulltext-indexing` - Process Meilisearch batch indexing jobs
379
+
380
+ Options:
381
+ - `--concurrency <n>` - Number of concurrent jobs to process (default: 1)
382
+
383
+ Examples:
384
+ ```bash
385
+ # Start vector indexing worker with 10 concurrent jobs
386
+ yarn mercato search worker vector-indexing --concurrency=10
387
+
388
+ # Start Meilisearch indexing worker with 5 concurrent jobs
389
+ yarn mercato search worker fulltext-indexing --concurrency=5
390
+ ```
391
+
392
+ **Requirements:**
393
+ - `QUEUE_STRATEGY=async` must be set in environment
394
+ - Redis must be configured via `REDIS_URL` or `QUEUE_REDIS_URL`
395
+
396
+ ### Help
397
+
398
+ Show all available commands:
399
+
400
+ ```bash
401
+ yarn mercato search help
402
+ ```
403
+
404
+ ## Queue Configuration
405
+
406
+ The search module supports two queue strategies:
407
+
408
+ ### Local Queue (Development)
409
+
410
+ File-based queue stored in `.queue/` directory. No additional configuration required.
411
+
412
+ ```env
413
+ QUEUE_STRATEGY=local
414
+ ```
415
+
416
+ ### Async Queue (Production)
417
+
418
+ Redis-based queue using BullMQ for distributed processing.
419
+
420
+ ```env
421
+ QUEUE_STRATEGY=async
422
+ REDIS_URL=redis://localhost:6379
423
+ ```
424
+
425
+ When using async queues, start workers in separate processes:
426
+
427
+ ```bash
428
+ # Terminal 1: Start vector indexing worker
429
+ yarn mercato search worker vector-indexing --concurrency=10
430
+
431
+ # Terminal 2: Start Meilisearch indexing worker
432
+ yarn mercato search worker fulltext-indexing --concurrency=5
433
+ ```
434
+
435
+ **Note:** If no workers are running when a reindex is triggered, the API will automatically fall back to synchronous processing and display a warning.
436
+
437
+ ## Environment Variables
438
+
439
+ | Variable | Description | Default |
440
+ |----------|-------------|---------|
441
+ | `MEILISEARCH_HOST` | Meilisearch server URL | - |
442
+ | `MEILISEARCH_API_KEY` | Meilisearch API key | - |
443
+ | `OPENAI_API_KEY` | OpenAI API key for embeddings | - |
444
+ | `OM_SEARCH_ENABLED` | Enable/disable search module | `true` |
445
+ | `OM_SEARCH_DEBUG` | Enable debug logging for search module | `false` |
446
+ | `SEARCH_EXCLUDE_ENCRYPTED_FIELDS` | Exclude encrypted fields from Meilisearch indexing | `false` |
447
+ | `QUEUE_STRATEGY` | Queue strategy (`local` or `async`) | `local` |
448
+ | `REDIS_URL` | Redis connection URL for async queues | - |
449
+ | `QUEUE_REDIS_URL` | Alternative Redis URL for queues | - |
450
+
451
+ ### Debug Logging
452
+
453
+ Set `OM_SEARCH_DEBUG=true` to enable verbose debug logging for the search module. This outputs detailed information about indexing operations, strategy selection, and error handling. Errors are always logged regardless of this flag.
454
+
455
+ ```env
456
+ OM_SEARCH_DEBUG=true
457
+ ```
458
+
459
+ ### Encrypted Field Exclusion
460
+
461
+ By default, all fields (including decrypted values of encrypted fields) are indexed into Meilisearch for full-text search. For security-sensitive deployments, you can exclude encrypted fields from indexing by setting:
462
+
463
+ ```env
464
+ SEARCH_EXCLUDE_ENCRYPTED_FIELDS=true
465
+ ```
466
+
467
+ When enabled, fields defined in `encryption_maps` are automatically excluded from the Meilisearch index. This includes fields like:
468
+ - Customer PII: `display_name`, `primary_email`, `primary_phone`
469
+ - Person profiles: `first_name`, `last_name`, `job_title`
470
+ - Company profiles: `legal_name`, `brand_name`, `domain`
471
+ - Comments and activities: `body`, `subject`
472
+ - And other fields configured in the encryption maps
473
+
474
+ **Note:** This only affects Meilisearch indexing. Vector search uses its own field configuration via `buildSource`. Consider adjusting your `buildSource` implementation if you also need to exclude sensitive fields from vector embeddings.
475
+
476
+ ## Configuring Entities for Search
477
+
478
+ Each module can define which entities are searchable by creating a `search.ts` file in the module root.
479
+
480
+ ### Entity Configuration Structure
481
+
482
+ ```typescript
483
+ // packages/your-package/src/modules/your-module/search.ts
484
+ import type {
485
+ SearchModuleConfig,
486
+ SearchBuildContext,
487
+ SearchIndexSource,
488
+ SearchResultPresenter,
489
+ SearchResultLink,
490
+ } from '@open-mercato/shared/modules/search'
491
+
492
+ export const searchConfig: SearchModuleConfig = {
493
+ entities: [
494
+ {
495
+ entityId: 'your_module:your_entity', // Must match entity registry
496
+ enabled: true,
497
+ priority: 10, // Higher = appears first in mixed results
498
+
499
+ // FOR VECTOR SEARCH: buildSource generates text for embeddings
500
+ buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {
501
+ const lines: string[] = []
502
+
503
+ // Add text that should be searchable semantically
504
+ lines.push(`Name: ${ctx.record.name}`)
505
+ lines.push(`Description: ${ctx.record.description}`)
506
+
507
+ // Include custom fields
508
+ if (ctx.customFields.notes) {
509
+ lines.push(`Notes: ${ctx.customFields.notes}`)
510
+ }
511
+
512
+ if (!lines.length) return null
513
+
514
+ return {
515
+ text: lines, // This text gets embedded for vector search
516
+ presenter: {
517
+ title: ctx.record.name,
518
+ subtitle: ctx.record.description,
519
+ icon: 'lucide:file',
520
+ },
521
+ links: [
522
+ { href: `/your-entity/${ctx.record.id}`, label: 'View', kind: 'primary' }
523
+ ],
524
+ checksumSource: { record: ctx.record, customFields: ctx.customFields },
525
+ }
526
+ },
527
+
528
+ // FOR MEILISEARCH: fieldPolicy controls full-text indexing
529
+ fieldPolicy: {
530
+ searchable: ['name', 'description', 'notes'], // Indexed for full-text
531
+ hashOnly: ['email', 'phone'], // Hashed, not searchable
532
+ excluded: ['password', 'secret'], // Never indexed
533
+ },
534
+
535
+ // Optional: Custom presenter formatting
536
+ formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {
537
+ return {
538
+ title: ctx.record.name,
539
+ subtitle: ctx.record.status,
540
+ icon: 'lucide:user',
541
+ }
542
+ },
543
+
544
+ // Optional: Primary URL for the record
545
+ resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {
546
+ return `/your-entity/${ctx.record.id}`
547
+ },
548
+
549
+ // Optional: Additional action links
550
+ resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {
551
+ return [
552
+ { href: `/your-entity/${ctx.record.id}/edit`, label: 'Edit', kind: 'secondary' }
553
+ ]
554
+ },
555
+ },
556
+ ],
557
+ }
558
+
559
+ export default searchConfig
560
+ ```
561
+
562
+ ### Search Strategies Comparison
563
+
564
+ | Aspect | Full-Text (fulltext) | Vector Search (vector) | Token Search (tokens) |
565
+ |--------|----------------------|------------------------|----------------------|
566
+ | **Configuration** | `fieldPolicy` | `buildSource` | Automatic |
567
+ | **Search Type** | Full-text with typo tolerance | Semantic similarity | Exact token matching |
568
+ | **Good For** | Exact matches, filters, facets | Natural language, "find similar" | Simple lookups |
569
+ | **Backend** | Meilisearch server | pgvector/Qdrant/ChromaDB + embeddings | PostgreSQL |
570
+ | **Requires** | `MEILISEARCH_HOST` | `OPENAI_API_KEY` (or other provider) | Database connection |
571
+
572
+ ### Enabling/Disabling Strategies
573
+
574
+ You can control which strategies are used at multiple levels:
575
+
576
+ #### Per Entity
577
+
578
+ ```typescript
579
+ // Full-text only (no vector search)
580
+ {
581
+ entityId: 'your_module:your_entity',
582
+ enabled: true,
583
+ // NO buildSource = no vector search
584
+ fieldPolicy: {
585
+ searchable: ['name', 'description'],
586
+ },
587
+ }
588
+
589
+ // Vector only (no full-text)
590
+ {
591
+ entityId: 'your_module:your_entity',
592
+ enabled: true,
593
+ buildSource: async (ctx) => ({ text: [...], presenter: {...} }),
594
+ // NO fieldPolicy = no full-text search
595
+ }
596
+
597
+ // Both strategies
598
+ {
599
+ entityId: 'your_module:your_entity',
600
+ enabled: true,
601
+ buildSource: async (ctx) => ({ text: [...], presenter: {...} }),
602
+ fieldPolicy: { searchable: ['name'] },
603
+ }
604
+ ```
605
+
606
+ #### Global Level (DI Registration)
607
+
608
+ In `packages/core/src/bootstrap.ts`:
609
+
610
+ ```typescript
611
+ import { registerSearchModule } from '@open-mercato/search'
612
+
613
+ registerSearchModule(container, {
614
+ moduleConfigs: searchModuleConfigs,
615
+ skipVector: true, // Disable vector search globally
616
+ skipFulltext: true, // Disable full-text search globally
617
+ skipTokens: true, // Disable token search globally
618
+ })
619
+ ```
620
+
621
+ #### Per Query
622
+
623
+ ```typescript
624
+ // Only use full-text search for this query
625
+ const results = await searchService.search('query', {
626
+ tenantId: '...',
627
+ strategies: ['fulltext'],
628
+ })
629
+
630
+ // Only use vector search
631
+ const results = await searchService.search('query', {
632
+ tenantId: '...',
633
+ strategies: ['vector'],
634
+ })
635
+
636
+ // Use all available strategies (default)
637
+ const results = await searchService.search('query', {
638
+ tenantId: '...',
639
+ })
640
+ ```
641
+
642
+ #### Environment-Based
643
+
644
+ Strategies automatically become unavailable if their backend is not configured:
645
+
646
+ | Strategy | Required Environment |
647
+ |----------|---------------------|
648
+ | fulltext | `MEILISEARCH_HOST` |
649
+ | vector | `OPENAI_API_KEY` (or other embedding provider) |
650
+ | tokens | Database connection (always available) |
651
+
652
+ ### SearchBuildContext
653
+
654
+ The context object passed to `buildSource` and other config functions:
655
+
656
+ ```typescript
657
+ interface SearchBuildContext {
658
+ record: Record<string, unknown> // The database record
659
+ customFields: Record<string, unknown> // Custom field values (cf:* fields)
660
+ tenantId?: string | null
661
+ organizationId?: string | null
662
+ queryEngine?: QueryEngine // For loading related entities
663
+ }
664
+ ```
665
+
666
+ ### SearchIndexSource
667
+
668
+ The return type from `buildSource`:
669
+
670
+ ```typescript
671
+ interface SearchIndexSource {
672
+ text: string | string[] // Text to embed for vector search
673
+ presenter?: SearchResultPresenter // Display info for search results
674
+ links?: SearchResultLink[] // Action links
675
+ checksumSource?: unknown // Used for change detection
676
+ }
677
+ ```
678
+
679
+ ## Architecture
680
+
681
+ ```
682
+ packages/search/
683
+ ├── src/
684
+ │ ├── modules/search/
685
+ │ │ ├── api/ # API routes
686
+ │ │ ├── cli.ts # CLI commands
687
+ │ │ ├── di.ts # Dependency injection
688
+ │ │ ├── subscribers/ # Event subscribers for auto-indexing
689
+ │ │ └── workers/ # Queue job handlers
690
+ │ ├── indexer/ # Search indexer implementation
691
+ │ ├── queue/ # Queue definitions
692
+ │ ├── strategies/ # Search strategy implementations
693
+ │ └── vector/ # Vector index service
694
+ ```
@@ -0,0 +1,10 @@
1
+ export const features = [
2
+ { id: 'search.view', title: 'View search settings', module: 'search' },
3
+ { id: 'search.manage', title: 'Manage search settings', module: 'search' },
4
+ { id: 'search.reindex', title: 'Reindex search data', module: 'search' },
5
+ { id: 'search.embeddings.view', title: 'View embedding settings', module: 'search' },
6
+ { id: 'search.embeddings.manage', title: 'Manage embedding settings', module: 'search' },
7
+ { id: 'search.global', title: 'Use global search (Cmd+K)', module: 'search' },
8
+ ]
9
+
10
+ export default features