@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,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
+
5
+ export const metadata = {
6
+ GET: { requireAuth: true, requireFeatures: ['search.view'] },
7
+ }
8
+
9
+ type EnvVarStatus = {
10
+ set: boolean
11
+ hint: string
12
+ }
13
+
14
+ type OptionalEnvVarStatus = {
15
+ set: boolean
16
+ value?: string | boolean
17
+ default?: string | boolean
18
+ hint: string
19
+ }
20
+
21
+ type FulltextConfigResponse = {
22
+ driver: 'meilisearch' | null
23
+ configured: boolean
24
+ envVars: {
25
+ MEILISEARCH_HOST: EnvVarStatus
26
+ MEILISEARCH_API_KEY: EnvVarStatus
27
+ }
28
+ optionalEnvVars: {
29
+ MEILISEARCH_INDEX_PREFIX: OptionalEnvVarStatus
30
+ SEARCH_EXCLUDE_ENCRYPTED_FIELDS: OptionalEnvVarStatus
31
+ }
32
+ }
33
+
34
+ const unauthorized = async () => {
35
+ const { t } = await resolveTranslations()
36
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
37
+ }
38
+
39
+ export async function GET(req: Request) {
40
+ const auth = await getAuthFromRequest(req)
41
+ if (!auth?.sub) return await unauthorized()
42
+
43
+ const hostSet = Boolean(process.env.MEILISEARCH_HOST?.trim())
44
+ const apiKeySet = Boolean(process.env.MEILISEARCH_API_KEY?.trim())
45
+ const configured = hostSet && apiKeySet
46
+
47
+ const indexPrefix = process.env.MEILISEARCH_INDEX_PREFIX?.trim()
48
+ const excludeEncrypted = ['1', 'true', 'yes', 'on'].includes(
49
+ (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? '').toLowerCase()
50
+ )
51
+
52
+ const response: FulltextConfigResponse = {
53
+ driver: configured ? 'meilisearch' : null,
54
+ configured,
55
+ envVars: {
56
+ MEILISEARCH_HOST: {
57
+ set: hostSet,
58
+ hint: 'The URL of your Meilisearch server (e.g., http://localhost:7700)',
59
+ },
60
+ MEILISEARCH_API_KEY: {
61
+ set: apiKeySet,
62
+ hint: 'API key for authentication with Meilisearch',
63
+ },
64
+ },
65
+ optionalEnvVars: {
66
+ MEILISEARCH_INDEX_PREFIX: {
67
+ set: Boolean(indexPrefix),
68
+ value: indexPrefix,
69
+ default: 'om',
70
+ hint: 'Prefix for index names to namespace indexes per tenant',
71
+ },
72
+ SEARCH_EXCLUDE_ENCRYPTED_FIELDS: {
73
+ set: Boolean(process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS),
74
+ value: excludeEncrypted,
75
+ default: false,
76
+ hint: 'Exclude encrypted fields from full-text indexing for security',
77
+ },
78
+ },
79
+ }
80
+
81
+ return NextResponse.json(response)
82
+ }
@@ -0,0 +1,91 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
6
+ import {
7
+ resolveGlobalSearchStrategies,
8
+ saveGlobalSearchStrategies,
9
+ DEFAULT_GLOBAL_SEARCH_STRATEGIES,
10
+ } from '../../../lib/global-search-config'
11
+ import type { SearchStrategyId } from '@open-mercato/shared/modules/search'
12
+
13
+ const updateSchema = z.object({
14
+ enabledStrategies: z.array(z.enum(['fulltext', 'vector', 'tokens'])).min(1),
15
+ })
16
+
17
+ export const metadata = {
18
+ GET: { requireAuth: true, requireFeatures: ['search.view'] },
19
+ POST: { requireAuth: true, requireFeatures: ['search.manage'] },
20
+ }
21
+
22
+ type SettingsResponse = {
23
+ enabledStrategies: SearchStrategyId[]
24
+ }
25
+
26
+ const toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)
27
+
28
+ const unauthorized = async () => {
29
+ const { t } = await resolveTranslations()
30
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
31
+ }
32
+
33
+ export async function GET(req: Request) {
34
+ const auth = await getAuthFromRequest(req)
35
+ if (!auth?.sub) return await unauthorized()
36
+
37
+ const container = await createRequestContainer()
38
+ try {
39
+ const enabledStrategies = await resolveGlobalSearchStrategies(container, {
40
+ defaultValue: DEFAULT_GLOBAL_SEARCH_STRATEGIES,
41
+ })
42
+
43
+ return toJson({ enabledStrategies })
44
+ } finally {
45
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
46
+ if (typeof disposable.dispose === 'function') {
47
+ await disposable.dispose()
48
+ }
49
+ }
50
+ }
51
+
52
+ export async function POST(req: Request) {
53
+ const { t } = await resolveTranslations()
54
+ const auth = await getAuthFromRequest(req)
55
+ if (!auth?.sub) return await unauthorized()
56
+
57
+ let body: unknown
58
+ try {
59
+ body = await req.json()
60
+ } catch {
61
+ return NextResponse.json(
62
+ { error: t('api.errors.invalidPayload', 'Invalid request body') },
63
+ { status: 400 }
64
+ )
65
+ }
66
+
67
+ const parsed = updateSchema.safeParse(body)
68
+ if (!parsed.success) {
69
+ return NextResponse.json(
70
+ { error: t('search.api.errors.invalidStrategies', 'Invalid strategies configuration') },
71
+ { status: 400 }
72
+ )
73
+ }
74
+
75
+ const container = await createRequestContainer()
76
+ try {
77
+ await saveGlobalSearchStrategies(container, parsed.data.enabledStrategies)
78
+
79
+ return NextResponse.json({ ok: true, enabledStrategies: parsed.data.enabledStrategies })
80
+ } catch (error) {
81
+ return NextResponse.json(
82
+ { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },
83
+ { status: 500 }
84
+ )
85
+ } finally {
86
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
87
+ if (typeof disposable.dispose === 'function') {
88
+ await disposable.dispose()
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,187 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
+ import type { SearchService } from '@open-mercato/search'
6
+ import type { FullTextSearchStrategy } from '@open-mercato/search/strategies'
7
+ import type { Knex } from 'knex'
8
+ import type { EntityManager } from '@mikro-orm/postgresql'
9
+ import { getReindexLockStatus } from '../../lib/reindex-lock'
10
+
11
+ export const metadata = {
12
+ GET: { requireAuth: true, requireFeatures: ['search.view'] },
13
+ }
14
+
15
+ type StrategyStatus = {
16
+ id: string
17
+ name: string
18
+ priority: number
19
+ available: boolean
20
+ }
21
+
22
+ type FulltextStats = {
23
+ numberOfDocuments: number
24
+ isIndexing: boolean
25
+ fieldDistribution: Record<string, number>
26
+ }
27
+
28
+ type ReindexLock = {
29
+ type: 'fulltext' | 'vector'
30
+ action: string
31
+ startedAt: string
32
+ elapsedMinutes: number
33
+ processedCount?: number | null
34
+ totalCount?: number | null
35
+ }
36
+
37
+ type SearchSettings = {
38
+ strategies: StrategyStatus[]
39
+ fulltextConfigured: boolean
40
+ fulltextStats: FulltextStats | null
41
+ vectorConfigured: boolean
42
+ tokensEnabled: boolean
43
+ defaultStrategies: string[]
44
+ /** @deprecated Use fulltextReindexLock or vectorReindexLock instead */
45
+ reindexLock: ReindexLock | null
46
+ fulltextReindexLock: ReindexLock | null
47
+ vectorReindexLock: ReindexLock | null
48
+ }
49
+
50
+ type SettingsResponse = {
51
+ settings: SearchSettings
52
+ }
53
+
54
+ const toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)
55
+
56
+ const unauthorized = async () => {
57
+ const { t } = await resolveTranslations()
58
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
59
+ }
60
+
61
+ export async function GET(req: Request) {
62
+ const auth = await getAuthFromRequest(req)
63
+ if (!auth?.sub) return await unauthorized()
64
+
65
+ const container = await createRequestContainer()
66
+ try {
67
+ const strategies: StrategyStatus[] = []
68
+ let defaultStrategies: string[] = []
69
+ let fulltextStats: FulltextStats | null = null
70
+
71
+ try {
72
+ const searchService = container.resolve('searchService') as SearchService | undefined
73
+ const searchStrategies = container.resolve('searchStrategies') as unknown[] | undefined
74
+
75
+ if (searchStrategies) {
76
+ for (const strategy of searchStrategies) {
77
+ const s = strategy as { id?: string; name?: string; priority?: number; isAvailable?: () => Promise<boolean> }
78
+ const available = await s.isAvailable?.() ?? true
79
+ strategies.push({
80
+ id: s.id ?? 'unknown',
81
+ name: s.name ?? s.id ?? 'unknown',
82
+ priority: s.priority ?? 0,
83
+ available,
84
+ })
85
+ }
86
+
87
+ // Get fulltext stats if available and tenant is set
88
+ if (auth.tenantId) {
89
+ const fulltextStrategy = searchStrategies.find(
90
+ (s: unknown) => (s as { id?: string })?.id === 'fulltext'
91
+ ) as FullTextSearchStrategy | undefined
92
+
93
+ if (fulltextStrategy) {
94
+ try {
95
+ const stats = await fulltextStrategy.getIndexStats(auth.tenantId)
96
+ if (stats) {
97
+ fulltextStats = stats
98
+ }
99
+ } catch {
100
+ // Stats not available
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ if (searchService) {
107
+ defaultStrategies = searchService.getDefaultStrategies?.() ?? []
108
+ }
109
+ } catch {
110
+ // Search service may not be available
111
+ }
112
+
113
+ const fulltextConfigured = Boolean(
114
+ process.env.MEILISEARCH_HOST && process.env.MEILISEARCH_HOST.trim().length > 0
115
+ )
116
+
117
+ const vectorConfigured = Boolean(
118
+ process.env.OPENAI_API_KEY ||
119
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY ||
120
+ process.env.MISTRAL_API_KEY ||
121
+ process.env.COHERE_API_KEY ||
122
+ process.env.AWS_ACCESS_KEY_ID ||
123
+ process.env.OLLAMA_BASE_URL
124
+ )
125
+
126
+ const tokensEnabled = process.env.OM_SEARCH_ENABLED !== 'false'
127
+
128
+ // Check for active reindex locks with heartbeat-based stale detection
129
+ let fulltextReindexLock: ReindexLock | null = null
130
+ let vectorReindexLock: ReindexLock | null = null
131
+
132
+ if (auth.tenantId) {
133
+ const em = container.resolve('em') as EntityManager
134
+ const knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()
135
+
136
+ // Check fulltext lock (auto-cleans stale locks based on heartbeat)
137
+ const fulltextLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: 'fulltext' })
138
+ if (fulltextLockStatus) {
139
+ const startedAt = new Date(fulltextLockStatus.startedAt)
140
+ fulltextReindexLock = {
141
+ type: 'fulltext',
142
+ action: fulltextLockStatus.action,
143
+ startedAt: fulltextLockStatus.startedAt,
144
+ elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),
145
+ processedCount: fulltextLockStatus.processedCount,
146
+ totalCount: fulltextLockStatus.totalCount,
147
+ }
148
+ }
149
+
150
+ // Check vector lock (auto-cleans stale locks based on heartbeat)
151
+ const vectorLockStatus = await getReindexLockStatus(knex, auth.tenantId, { type: 'vector' })
152
+ if (vectorLockStatus) {
153
+ const startedAt = new Date(vectorLockStatus.startedAt)
154
+ vectorReindexLock = {
155
+ type: 'vector',
156
+ action: vectorLockStatus.action,
157
+ startedAt: vectorLockStatus.startedAt,
158
+ elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),
159
+ processedCount: vectorLockStatus.processedCount,
160
+ totalCount: vectorLockStatus.totalCount,
161
+ }
162
+ }
163
+ }
164
+
165
+ // Keep deprecated reindexLock for backwards compatibility (prefer fulltext if both are active)
166
+ const reindexLock = fulltextReindexLock ?? vectorReindexLock
167
+
168
+ return toJson({
169
+ settings: {
170
+ strategies,
171
+ fulltextConfigured,
172
+ fulltextStats,
173
+ vectorConfigured,
174
+ tokensEnabled,
175
+ defaultStrategies,
176
+ reindexLock,
177
+ fulltextReindexLock,
178
+ vectorReindexLock,
179
+ },
180
+ })
181
+ } finally {
182
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
183
+ if (typeof disposable.dispose === 'function') {
184
+ await disposable.dispose()
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,105 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
+ import type { VectorDriverId } from '@open-mercato/shared/modules/vector'
5
+
6
+ export const metadata = {
7
+ GET: { requireAuth: true, requireFeatures: ['search.view'] },
8
+ }
9
+
10
+ type DriverStatus = {
11
+ id: VectorDriverId
12
+ name: string
13
+ configured: boolean
14
+ implemented: boolean
15
+ envVars: {
16
+ name: string
17
+ set: boolean
18
+ hint: string
19
+ }[]
20
+ }
21
+
22
+ type VectorStoreConfigResponse = {
23
+ currentDriver: VectorDriverId
24
+ configured: boolean
25
+ drivers: DriverStatus[]
26
+ }
27
+
28
+ const unauthorized = async () => {
29
+ const { t } = await resolveTranslations()
30
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
31
+ }
32
+
33
+ export async function GET(req: Request) {
34
+ const auth = await getAuthFromRequest(req)
35
+ if (!auth?.sub) return await unauthorized()
36
+
37
+ // Check pgvector - uses existing DATABASE_URL
38
+ const databaseUrlSet = Boolean(process.env.DATABASE_URL?.trim())
39
+
40
+ // Check qdrant - would need QDRANT_URL
41
+ const qdrantUrlSet = Boolean(process.env.QDRANT_URL?.trim())
42
+ const qdrantApiKeySet = Boolean(process.env.QDRANT_API_KEY?.trim())
43
+
44
+ // Check chromadb - would need CHROMA_URL
45
+ const chromaUrlSet = Boolean(process.env.CHROMA_URL?.trim())
46
+
47
+ const drivers: DriverStatus[] = [
48
+ {
49
+ id: 'pgvector',
50
+ name: 'PostgreSQL (pgvector)',
51
+ configured: databaseUrlSet,
52
+ implemented: true,
53
+ envVars: [
54
+ {
55
+ name: 'DATABASE_URL',
56
+ set: databaseUrlSet,
57
+ hint: 'PostgreSQL connection string with pgvector extension',
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ id: 'qdrant',
63
+ name: 'Qdrant',
64
+ configured: qdrantUrlSet,
65
+ implemented: false,
66
+ envVars: [
67
+ {
68
+ name: 'QDRANT_URL',
69
+ set: qdrantUrlSet,
70
+ hint: 'URL of your Qdrant server (e.g., http://localhost:6333)',
71
+ },
72
+ {
73
+ name: 'QDRANT_API_KEY',
74
+ set: qdrantApiKeySet,
75
+ hint: 'API key for Qdrant authentication (optional for local)',
76
+ },
77
+ ],
78
+ },
79
+ {
80
+ id: 'chromadb',
81
+ name: 'ChromaDB',
82
+ configured: chromaUrlSet,
83
+ implemented: false,
84
+ envVars: [
85
+ {
86
+ name: 'CHROMA_URL',
87
+ set: chromaUrlSet,
88
+ hint: 'URL of your ChromaDB server (e.g., http://localhost:8000)',
89
+ },
90
+ ],
91
+ },
92
+ ]
93
+
94
+ // Currently only pgvector is supported
95
+ const currentDriver: VectorDriverId = 'pgvector'
96
+ const configured = databaseUrlSet
97
+
98
+ const response: VectorStoreConfigResponse = {
99
+ currentDriver,
100
+ configured,
101
+ drivers,
102
+ }
103
+
104
+ return NextResponse.json(response)
105
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+
3
+ const searchIcon = React.createElement(
4
+ 'svg',
5
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
6
+ React.createElement('circle', { cx: 11, cy: 11, r: 8 }),
7
+ React.createElement('path', { d: 'm21 21-4.3-4.3' }),
8
+ )
9
+
10
+ export const metadata = {
11
+ requireAuth: true,
12
+ requireFeatures: ['search.view'],
13
+ pageTitle: 'Search Settings',
14
+ pageTitleKey: 'search.config.nav.hybridSearch',
15
+ pageGroup: 'Configuration',
16
+ pageGroupKey: 'backend.nav.configuration',
17
+ pageOrder: 425,
18
+ icon: searchIcon,
19
+ breadcrumb: [
20
+ { label: 'Search Settings', labelKey: 'search.config.nav.hybridSearch' },
21
+ ],
22
+ } as const
@@ -0,0 +1,12 @@
1
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
2
+ import { SearchSettingsPageClient } from '../../../frontend/components/SearchSettingsPageClient'
3
+
4
+ export default async function SearchSettingsPage() {
5
+ return (
6
+ <Page>
7
+ <PageBody>
8
+ <SearchSettingsPageClient />
9
+ </PageBody>
10
+ </Page>
11
+ )
12
+ }