@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/build.mjs ADDED
@@ -0,0 +1,92 @@
1
+ import * as esbuild from 'esbuild'
2
+ import { glob } from 'glob'
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs'
4
+ import { dirname, join, relative } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+
9
+ const entryPoints = await glob('src/**/*.{ts,tsx}', {
10
+ ignore: ['**/__tests__/**', '**/*.test.ts', '**/*.test.tsx']
11
+ })
12
+
13
+ // Plugin to add .js extension to relative imports
14
+ const addJsExtension = {
15
+ name: 'add-js-extension',
16
+ setup(build) {
17
+ build.onEnd(async (result) => {
18
+ if (result.errors.length > 0) return
19
+ const outputFiles = await glob('dist/**/*.js')
20
+ for (const file of outputFiles) {
21
+ const fileDir = dirname(file)
22
+ let content = readFileSync(file, 'utf-8')
23
+ // Add .js to relative imports that don't have an extension
24
+ content = content.replace(
25
+ /from\s+["'](\.[^"']+)["']/g,
26
+ (match, path) => {
27
+ if (path.endsWith('.js') || path.endsWith('.json')) return match
28
+ // Check if it's a directory with index.js
29
+ const resolvedPath = join(fileDir, path)
30
+ if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
31
+ return `from "${path}/index.js"`
32
+ }
33
+ return `from "${path}.js"`
34
+ }
35
+ )
36
+ content = content.replace(
37
+ /import\s*\(\s*["'](\.[^"']+)["']\s*\)/g,
38
+ (match, path) => {
39
+ if (path.endsWith('.js') || path.endsWith('.json')) return match
40
+ // Check if it's a directory with index.js
41
+ const resolvedPath = join(fileDir, path)
42
+ if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
43
+ return `import("${path}/index.js")`
44
+ }
45
+ return `import("${path}.js")`
46
+ }
47
+ )
48
+ // Handle side-effect imports: import "./path" (no from clause)
49
+ content = content.replace(
50
+ /import\s+["'](\.[^"']+)["'];/g,
51
+ (match, path) => {
52
+ if (path.endsWith('.js') || path.endsWith('.json')) return match
53
+ // Check if it's a directory with index.js
54
+ const resolvedPath = join(fileDir, path)
55
+ if (existsSync(resolvedPath) && existsSync(join(resolvedPath, 'index.js'))) {
56
+ return `import "${path}/index.js";`
57
+ }
58
+ return `import "${path}.js";`
59
+ }
60
+ )
61
+ writeFileSync(file, content)
62
+ }
63
+ })
64
+ }
65
+ }
66
+
67
+ const outdir = join(__dirname, 'dist')
68
+
69
+ await esbuild.build({
70
+ entryPoints,
71
+ outdir,
72
+ outbase: join(__dirname, 'src'),
73
+ format: 'esm',
74
+ platform: 'node',
75
+ target: 'node18',
76
+ sourcemap: true,
77
+ jsx: 'automatic',
78
+ plugins: [addJsExtension],
79
+ })
80
+
81
+ // Copy JSON files from src to dist (esbuild doesn't handle non-entry JSON files)
82
+ const jsonFiles = await glob(join(__dirname, 'src/**/*.json'), {
83
+ ignore: ['**/node_modules/**']
84
+ })
85
+ for (const jsonFile of jsonFiles) {
86
+ const relativePath = relative(join(__dirname, 'src'), jsonFile)
87
+ const destPath = join(outdir, relativePath)
88
+ mkdirSync(dirname(destPath), { recursive: true })
89
+ copyFileSync(jsonFile, destPath)
90
+ }
91
+
92
+ console.log('search built successfully')
package/dist/di.js ADDED
@@ -0,0 +1,157 @@
1
+ import { asValue } from "awilix";
2
+ import { SearchService } from "./service.js";
3
+ import { TokenSearchStrategy } from "./strategies/token.strategy.js";
4
+ import { VectorSearchStrategy } from "./strategies/vector.strategy.js";
5
+ import { FullTextSearchStrategy } from "./strategies/fulltext.strategy.js";
6
+ import { createFulltextDriver } from "./fulltext/drivers/index.js";
7
+ import { SearchIndexer } from "./indexer/search-indexer.js";
8
+ import { createPresenterEnricher } from "./lib/presenter-enricher.js";
9
+ function shouldExcludeEncryptedFields() {
10
+ const raw = (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? "").toLowerCase();
11
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
12
+ }
13
+ function createEncryptionMapResolver(knex) {
14
+ const cache = /* @__PURE__ */ new Map();
15
+ const CACHE_TTL_MS = 5 * 60 * 1e3;
16
+ return async (entityId) => {
17
+ const cached = cache.get(entityId);
18
+ if (cached && cached.expiresAt > Date.now()) {
19
+ return cached.entries;
20
+ }
21
+ try {
22
+ const rows = await knex("encryption_maps").select("fields_json").where("entity_id", entityId).where("is_active", true).whereNull("deleted_at").first();
23
+ const fieldsJson = rows?.fields_json;
24
+ const entries = Array.isArray(fieldsJson) ? fieldsJson.map((f) => ({
25
+ field: f.field,
26
+ hashField: f.hashField ?? null
27
+ })) : [];
28
+ cache.set(entityId, { entries, expiresAt: Date.now() + CACHE_TTL_MS });
29
+ return entries;
30
+ } catch {
31
+ return [];
32
+ }
33
+ };
34
+ }
35
+ function registerSearchModule(container, options) {
36
+ const strategies = [];
37
+ if (!options?.skipTokens) {
38
+ try {
39
+ const em = container.resolve("em");
40
+ const knex = em.getConnection().getKnex();
41
+ strategies.push(new TokenSearchStrategy(knex));
42
+ } catch {
43
+ }
44
+ }
45
+ if (!options?.skipVector) {
46
+ try {
47
+ const embeddingService = container.resolve("vectorEmbeddingService");
48
+ const drivers = container.resolve("vectorDrivers");
49
+ const primaryDriver = drivers?.[0];
50
+ if (embeddingService && primaryDriver) {
51
+ strategies.push(new VectorSearchStrategy(embeddingService, primaryDriver));
52
+ }
53
+ } catch {
54
+ }
55
+ }
56
+ const entityConfigMap = /* @__PURE__ */ new Map();
57
+ for (const moduleConfig of options?.moduleConfigs ?? []) {
58
+ for (const entityConfig of moduleConfig.entities) {
59
+ if (entityConfig.enabled !== false) {
60
+ entityConfigMap.set(entityConfig.entityId, entityConfig);
61
+ }
62
+ }
63
+ }
64
+ if (!options?.skipFulltext) {
65
+ let encryptionMapResolver;
66
+ if (shouldExcludeEncryptedFields()) {
67
+ try {
68
+ const em = container.resolve("em");
69
+ const knex = em.getConnection().getKnex();
70
+ encryptionMapResolver = createEncryptionMapResolver(knex);
71
+ } catch {
72
+ }
73
+ }
74
+ const fulltextDriver = createFulltextDriver({
75
+ fieldPolicyResolver: (entityId) => {
76
+ const config = entityConfigMap.get(entityId);
77
+ return config?.fieldPolicy;
78
+ },
79
+ encryptionMapResolver
80
+ });
81
+ if (fulltextDriver) {
82
+ strategies.push(new FullTextSearchStrategy(fulltextDriver));
83
+ }
84
+ }
85
+ const defaultStrategies = options?.defaultStrategies ?? determineDefaultStrategies(strategies);
86
+ let queryEngine;
87
+ try {
88
+ queryEngine = container.resolve("queryEngine");
89
+ } catch {
90
+ }
91
+ let encryptionService = null;
92
+ try {
93
+ encryptionService = container.resolve("tenantEncryptionService");
94
+ } catch {
95
+ }
96
+ let presenterEnricher;
97
+ try {
98
+ const em = container.resolve("em");
99
+ const knex = em.getConnection().getKnex();
100
+ presenterEnricher = createPresenterEnricher(knex, entityConfigMap, queryEngine, encryptionService);
101
+ } catch {
102
+ }
103
+ const searchService = new SearchService({
104
+ strategies,
105
+ defaultStrategies,
106
+ fallbackStrategy: "tokens",
107
+ mergeConfig: options?.mergeConfig ?? {
108
+ duplicateHandling: "highest_score",
109
+ strategyWeights: {
110
+ fulltext: 1.2,
111
+ vector: 1,
112
+ tokens: 0.8
113
+ }
114
+ },
115
+ presenterEnricher
116
+ });
117
+ const moduleConfigs = options?.moduleConfigs ?? [];
118
+ let fulltextQueue;
119
+ try {
120
+ fulltextQueue = container.resolve("fulltextIndexQueue");
121
+ } catch {
122
+ }
123
+ let vectorQueue;
124
+ try {
125
+ vectorQueue = container.resolve("vectorIndexQueue");
126
+ } catch {
127
+ }
128
+ const searchIndexer = new SearchIndexer(searchService, moduleConfigs, {
129
+ queryEngine,
130
+ fulltextQueue,
131
+ vectorQueue
132
+ });
133
+ container.register({
134
+ searchService: asValue(searchService),
135
+ searchStrategies: asValue(strategies),
136
+ searchIndexer: asValue(searchIndexer)
137
+ });
138
+ }
139
+ function determineDefaultStrategies(strategies) {
140
+ const available = new Set(strategies.map((s) => s.id));
141
+ const defaults = [];
142
+ if (available.has("fulltext")) defaults.push("fulltext");
143
+ if (available.has("vector")) defaults.push("vector");
144
+ if (available.has("tokens")) defaults.push("tokens");
145
+ return defaults.length > 0 ? defaults : ["tokens"];
146
+ }
147
+ function addSearchStrategy(container, strategy) {
148
+ const service = container.resolve("searchService");
149
+ service.registerStrategy(strategy);
150
+ const strategies = container.resolve("searchStrategies");
151
+ strategies.push(strategy);
152
+ }
153
+ export {
154
+ addSearchStrategy,
155
+ registerSearchModule
156
+ };
157
+ //# sourceMappingURL=di.js.map
package/dist/di.js.map ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/di.ts"],
4
+ "sourcesContent": ["import { asValue } from 'awilix'\nimport type { Knex } from 'knex'\nimport { SearchService } from './service'\nimport { TokenSearchStrategy } from './strategies/token.strategy'\nimport { VectorSearchStrategy, type EmbeddingService } from './strategies/vector.strategy'\nimport { FullTextSearchStrategy } from './strategies/fulltext.strategy'\nimport { createFulltextDriver } from './fulltext/drivers'\nimport { SearchIndexer } from './indexer/search-indexer'\nimport type {\n SearchStrategy,\n ResultMergeConfig,\n SearchModuleConfig,\n SearchFieldPolicy,\n SearchEntityConfig,\n PresenterEnricherFn,\n} from './types'\nimport type { VectorDriver } from './vector/types'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from './queue/fulltext-indexing'\nimport type { VectorIndexJobPayload } from './queue/vector-indexing'\nimport type { EncryptionMapEntry } from './lib/field-policy'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createPresenterEnricher } from './lib/presenter-enricher'\n\n/**\n * Check if encrypted fields should be excluded from search indexing.\n * Controlled by SEARCH_EXCLUDE_ENCRYPTED_FIELDS environment variable.\n * Default: false (index all fields including decrypted data)\n */\nfunction shouldExcludeEncryptedFields(): boolean {\n const raw = (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? '').toLowerCase()\n return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'\n}\n\n/**\n * Create an encryption map resolver that queries the database.\n * Falls back to empty array if query fails.\n */\nfunction createEncryptionMapResolver(\n knex: Knex,\n): (entityId: EntityId) => Promise<EncryptionMapEntry[]> {\n // Cache encryption maps per entity to avoid repeated queries\n const cache = new Map<string, { entries: EncryptionMapEntry[]; expiresAt: number }>()\n const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes\n\n return async (entityId: EntityId): Promise<EncryptionMapEntry[]> => {\n const cached = cache.get(entityId)\n if (cached && cached.expiresAt > Date.now()) {\n return cached.entries\n }\n\n try {\n const rows = await knex('encryption_maps')\n .select('fields_json')\n .where('entity_id', entityId)\n .where('is_active', true)\n .whereNull('deleted_at')\n .first()\n\n const fieldsJson = rows?.fields_json\n const entries: EncryptionMapEntry[] = Array.isArray(fieldsJson)\n ? fieldsJson.map((f: { field: string; hashField?: string | null }) => ({\n field: f.field,\n hashField: f.hashField ?? null,\n }))\n : []\n\n cache.set(entityId, { entries, expiresAt: Date.now() + CACHE_TTL_MS })\n return entries\n } catch {\n // Query failed, return empty array (don't exclude any fields)\n return []\n }\n }\n}\n\n/**\n * Container interface - minimal subset needed for registration.\n */\nexport interface SearchContainer {\n resolve<T = unknown>(name: string): T\n register(registrations: Record<string, unknown>): void\n}\n\n/**\n * Configuration options for search module registration.\n */\nexport type SearchModuleOptions = {\n /** Override default strategies to use */\n defaultStrategies?: string[]\n /** Override merge configuration */\n mergeConfig?: ResultMergeConfig\n /** Skip token strategy registration */\n skipTokens?: boolean\n /** Skip vector strategy registration */\n skipVector?: boolean\n /** Skip fulltext strategy registration */\n skipFulltext?: boolean\n /** Module configurations (from generated/search.generated.ts) */\n moduleConfigs?: SearchModuleConfig[]\n}\n\n/**\n * Register the search module in the DI container.\n *\n * This creates and registers:\n * - SearchService instance\n * - All configured search strategies\n *\n * @param container - Awilix container\n * @param options - Optional configuration overrides\n */\nexport function registerSearchModule(\n container: SearchContainer,\n options?: SearchModuleOptions,\n): void {\n const strategies: SearchStrategy[] = []\n\n // Token strategy (always available unless explicitly skipped)\n if (!options?.skipTokens) {\n try {\n const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')\n const knex = em.getConnection().getKnex()\n strategies.push(new TokenSearchStrategy(knex))\n } catch {\n // knex not available via em, skipping TokenSearchStrategy\n }\n }\n\n // Vector strategy (requires embedding service and driver)\n // Note: We register even if not currently available - availability is checked at search time\n // via isAvailable(). The embedding config may be loaded later from the database.\n if (!options?.skipVector) {\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const primaryDriver = drivers?.[0]\n\n if (embeddingService && primaryDriver) {\n strategies.push(new VectorSearchStrategy(embeddingService, primaryDriver))\n }\n } catch {\n // Vector module not available, skipping VectorSearchStrategy\n }\n }\n\n // Build entity config map for field policy resolution\n const entityConfigMap = new Map<EntityId, SearchEntityConfig>()\n for (const moduleConfig of (options?.moduleConfigs ?? [])) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n\n // Fulltext strategy (requires driver configuration, e.g., MEILISEARCH_HOST)\n if (!options?.skipFulltext) {\n // Build encryption map resolver if SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled\n let encryptionMapResolver: ((entityId: EntityId) => Promise<EncryptionMapEntry[]>) | undefined\n if (shouldExcludeEncryptedFields()) {\n try {\n const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')\n const knex = em.getConnection().getKnex()\n encryptionMapResolver = createEncryptionMapResolver(knex)\n } catch {\n // Knex not available, encrypted field filtering disabled\n }\n }\n\n const fulltextDriver = createFulltextDriver({\n fieldPolicyResolver: (entityId: EntityId): SearchFieldPolicy | undefined => {\n const config = entityConfigMap.get(entityId)\n return config?.fieldPolicy\n },\n encryptionMapResolver,\n })\n\n if (fulltextDriver) {\n strategies.push(new FullTextSearchStrategy(fulltextDriver))\n }\n }\n\n // Determine default strategies based on what's available\n const defaultStrategies = options?.defaultStrategies ?? determineDefaultStrategies(strategies)\n\n // Try to resolve queryEngine for reindex support and presenter enrichment\n let queryEngine: QueryEngine | undefined\n try {\n queryEngine = container.resolve<QueryEngine>('queryEngine')\n } catch {\n // QueryEngine not available, reindex will be disabled\n }\n\n // Resolve encryption service for decrypting presenter data\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = container.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n // Encryption service not available, presenters won't be decrypted\n }\n\n // Create presenter enricher for database-based presenter resolution\n let presenterEnricher: PresenterEnricherFn | undefined\n try {\n const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')\n const knex = em.getConnection().getKnex()\n presenterEnricher = createPresenterEnricher(knex, entityConfigMap, queryEngine, encryptionService)\n } catch {\n // knex not available, presenter enrichment disabled\n }\n\n // Create search service\n const searchService = new SearchService({\n strategies,\n defaultStrategies,\n fallbackStrategy: 'tokens',\n mergeConfig: options?.mergeConfig ?? {\n duplicateHandling: 'highest_score',\n strategyWeights: {\n fulltext: 1.2,\n vector: 1.0,\n tokens: 0.8,\n },\n },\n presenterEnricher,\n })\n\n // Create search indexer with module configs\n const moduleConfigs = options?.moduleConfigs ?? []\n\n // Try to resolve fulltextIndexQueue for queue-based reindexing\n let fulltextQueue: Queue<FulltextIndexJobPayload> | undefined\n try {\n fulltextQueue = container.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')\n } catch {\n // Queue not available, queue-based fulltext reindex will be disabled\n }\n\n // Try to resolve vectorIndexQueue for queue-based vector reindexing\n let vectorQueue: Queue<VectorIndexJobPayload> | undefined\n try {\n vectorQueue = container.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n // Queue not available, queue-based vector reindex will be disabled\n }\n\n const searchIndexer = new SearchIndexer(searchService, moduleConfigs, {\n queryEngine,\n fulltextQueue,\n vectorQueue,\n })\n\n // Register in container\n container.register({\n searchService: asValue(searchService),\n searchStrategies: asValue(strategies),\n searchIndexer: asValue(searchIndexer),\n })\n}\n\n/**\n * Determine default strategy order based on available strategies.\n * Prefers fulltext > vector > tokens.\n */\nfunction determineDefaultStrategies(strategies: SearchStrategy[]): string[] {\n const available = new Set(strategies.map((s) => s.id))\n const defaults: string[] = []\n\n if (available.has('fulltext')) defaults.push('fulltext')\n if (available.has('vector')) defaults.push('vector')\n if (available.has('tokens')) defaults.push('tokens')\n\n return defaults.length > 0 ? defaults : ['tokens']\n}\n\n/**\n * Helper to add a custom strategy to an existing SearchService.\n *\n * @param container - DI container\n * @param strategy - Strategy to add\n */\nexport function addSearchStrategy(container: SearchContainer, strategy: SearchStrategy): void {\n const service = container.resolve<SearchService>('searchService')\n service.registerStrategy(strategy)\n\n const strategies = container.resolve<SearchStrategy[]>('searchStrategies')\n strategies.push(strategy)\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,21 @@
1
+ import { createMeilisearchDriver } from "./meilisearch/index.js";
2
+ import { createMeilisearchDriver as createMeilisearchDriver2 } from "./meilisearch/index.js";
3
+ function createFulltextDriver(options) {
4
+ const meilisearchHost = options?.meilisearch?.host ?? process.env.MEILISEARCH_HOST;
5
+ if (meilisearchHost) {
6
+ return createMeilisearchDriver({
7
+ host: meilisearchHost,
8
+ apiKey: options?.meilisearch?.apiKey ?? process.env.MEILISEARCH_API_KEY,
9
+ indexPrefix: options?.meilisearch?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX,
10
+ encryptionMapResolver: options?.encryptionMapResolver,
11
+ fieldPolicyResolver: options?.fieldPolicyResolver,
12
+ defaultLimit: options?.defaultLimit
13
+ });
14
+ }
15
+ return null;
16
+ }
17
+ export {
18
+ createFulltextDriver,
19
+ createMeilisearchDriver2 as createMeilisearchDriver
20
+ };
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/fulltext/drivers/index.ts"],
4
+ "sourcesContent": ["import type { FullTextSearchDriver, FullTextSearchDriverConfig } from '../types'\nimport { createMeilisearchDriver } from './meilisearch'\n\nexport { createMeilisearchDriver, type MeilisearchDriverOptions } from './meilisearch'\n\nexport type FulltextDriverFactoryOptions = FullTextSearchDriverConfig & {\n meilisearch?: {\n host?: string\n apiKey?: string\n indexPrefix?: string\n }\n algolia?: {\n appId?: string\n apiKey?: string\n indexPrefix?: string\n }\n}\n\nexport function createFulltextDriver(\n options?: FulltextDriverFactoryOptions\n): FullTextSearchDriver | null {\n const meilisearchHost = options?.meilisearch?.host ?? process.env.MEILISEARCH_HOST\n\n if (meilisearchHost) {\n return createMeilisearchDriver({\n host: meilisearchHost,\n apiKey: options?.meilisearch?.apiKey ?? process.env.MEILISEARCH_API_KEY,\n indexPrefix: options?.meilisearch?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX,\n encryptionMapResolver: options?.encryptionMapResolver,\n fieldPolicyResolver: options?.fieldPolicyResolver,\n defaultLimit: options?.defaultLimit,\n })\n }\n\n // Future: Add Algolia, Elasticsearch, Typesense drivers here\n // if (options?.algolia?.appId || process.env.ALGOLIA_APP_ID) {\n // return createAlgoliaDriver({ ... })\n // }\n\n return null\n}\n"],
5
+ "mappings": "AACA,SAAS,+BAA+B;AAExC,SAAS,2BAAAA,gCAA8D;AAehE,SAAS,qBACd,SAC6B;AAC7B,QAAM,kBAAkB,SAAS,aAAa,QAAQ,QAAQ,IAAI;AAElE,MAAI,iBAAiB;AACnB,WAAO,wBAAwB;AAAA,MAC7B,MAAM;AAAA,MACN,QAAQ,SAAS,aAAa,UAAU,QAAQ,IAAI;AAAA,MACpD,aAAa,SAAS,aAAa,eAAe,QAAQ,IAAI;AAAA,MAC9D,uBAAuB,SAAS;AAAA,MAChC,qBAAqB,SAAS;AAAA,MAC9B,cAAc,SAAS;AAAA,IACzB,CAAC;AAAA,EACH;AAOA,SAAO;AACT;",
6
+ "names": ["createMeilisearchDriver"]
7
+ }
@@ -0,0 +1,320 @@
1
+ import { MeiliSearch } from "meilisearch";
2
+ import { extractSearchableFields } from "../../../lib/field-policy.js";
3
+ function createMeilisearchDriver(options) {
4
+ const host = options?.host ?? process.env.MEILISEARCH_HOST ?? "";
5
+ const apiKey = options?.apiKey ?? process.env.MEILISEARCH_API_KEY ?? "";
6
+ const indexPrefix = options?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX ?? "om";
7
+ const defaultLimit = options?.defaultLimit ?? 20;
8
+ const encryptionMapResolver = options?.encryptionMapResolver;
9
+ const fieldPolicyResolver = options?.fieldPolicyResolver;
10
+ let client = null;
11
+ const initializedIndexes = /* @__PURE__ */ new Set();
12
+ const initializingIndexes = /* @__PURE__ */ new Map();
13
+ function getClient() {
14
+ if (!client) {
15
+ client = new MeiliSearch({ host, apiKey });
16
+ }
17
+ return client;
18
+ }
19
+ function buildIndexName(tenantId) {
20
+ const sanitized = tenantId.replace(/[^a-zA-Z0-9_-]/g, "_");
21
+ return `${indexPrefix}_${sanitized}`;
22
+ }
23
+ function escapeFilterValue(value) {
24
+ return value.replace(/["\\]/g, "\\$&");
25
+ }
26
+ function buildFilters(options2) {
27
+ const filters = [];
28
+ if (options2.organizationId) {
29
+ filters.push(`_organizationId = "${escapeFilterValue(options2.organizationId)}"`);
30
+ }
31
+ if (options2.entityTypes?.length) {
32
+ const entityFilter = options2.entityTypes.map((t) => `"${escapeFilterValue(t)}"`).join(", ");
33
+ filters.push(`_entityId IN [${entityFilter}]`);
34
+ }
35
+ return filters;
36
+ }
37
+ async function doEnsureIndex(indexName) {
38
+ const meiliClient = getClient();
39
+ try {
40
+ await meiliClient.createIndex(indexName, { primaryKey: "_id" });
41
+ } catch (error) {
42
+ const meilisearchError = error;
43
+ if (meilisearchError.code !== "index_already_exists") {
44
+ throw error;
45
+ }
46
+ }
47
+ const index = meiliClient.index(indexName);
48
+ await index.updateSettings({
49
+ searchableAttributes: ["*"],
50
+ filterableAttributes: ["_entityId", "_organizationId"],
51
+ sortableAttributes: ["_indexedAt"],
52
+ typoTolerance: {
53
+ enabled: true,
54
+ minWordSizeForTypos: {
55
+ oneTypo: 4,
56
+ twoTypos: 8
57
+ }
58
+ }
59
+ });
60
+ initializedIndexes.add(indexName);
61
+ }
62
+ async function ensureIndex(indexName) {
63
+ if (initializedIndexes.has(indexName)) {
64
+ return;
65
+ }
66
+ const existingPromise = initializingIndexes.get(indexName);
67
+ if (existingPromise) {
68
+ return existingPromise;
69
+ }
70
+ const initPromise = doEnsureIndex(indexName);
71
+ initializingIndexes.set(indexName, initPromise);
72
+ try {
73
+ await initPromise;
74
+ } finally {
75
+ initializingIndexes.delete(indexName);
76
+ }
77
+ }
78
+ async function prepareDocument(doc) {
79
+ const excludeEncrypted = Boolean(encryptionMapResolver);
80
+ const encryptedFields = encryptionMapResolver ? await encryptionMapResolver(doc.entityId) : [];
81
+ const fieldPolicy = fieldPolicyResolver?.(doc.entityId);
82
+ const searchableFields = extractSearchableFields(doc.fields, {
83
+ encryptedFields,
84
+ fieldPolicy
85
+ });
86
+ let presenter = doc.presenter;
87
+ let links = doc.links;
88
+ if (excludeEncrypted) {
89
+ if (presenter) {
90
+ presenter = {
91
+ ...presenter,
92
+ title: "",
93
+ // Will be enriched at search time
94
+ subtitle: void 0
95
+ // Will be enriched at search time
96
+ };
97
+ }
98
+ if (links && links.length > 0) {
99
+ links = links.map((link) => ({
100
+ ...link,
101
+ label: link.kind === "primary" ? "Open" : "View"
102
+ // Generic labels
103
+ }));
104
+ }
105
+ }
106
+ return {
107
+ _id: doc.recordId,
108
+ _entityId: doc.entityId,
109
+ _organizationId: doc.organizationId,
110
+ _presenter: presenter,
111
+ _url: doc.url,
112
+ _links: links,
113
+ _indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
114
+ ...searchableFields
115
+ };
116
+ }
117
+ const driver = {
118
+ id: "meilisearch",
119
+ async ensureReady() {
120
+ },
121
+ async isHealthy() {
122
+ if (!host) {
123
+ return false;
124
+ }
125
+ try {
126
+ const meiliClient = getClient();
127
+ await meiliClient.health();
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ },
133
+ async search(query, options2) {
134
+ const meiliClient = getClient();
135
+ const indexName = buildIndexName(options2.tenantId);
136
+ try {
137
+ const index = meiliClient.index(indexName);
138
+ const filters = buildFilters(options2);
139
+ const response = await index.search(query, {
140
+ limit: options2.limit ?? defaultLimit,
141
+ offset: options2.offset,
142
+ filter: filters.length > 0 ? filters.join(" AND ") : void 0,
143
+ showRankingScore: true
144
+ });
145
+ return response.hits.map((hit) => ({
146
+ recordId: hit._id,
147
+ entityId: hit._entityId,
148
+ score: hit._rankingScore ?? 0.5,
149
+ presenter: hit._presenter,
150
+ url: hit._url,
151
+ links: hit._links,
152
+ metadata: hit._metadata
153
+ }));
154
+ } catch (error) {
155
+ const meilisearchError = error;
156
+ if (meilisearchError.code === "index_not_found") {
157
+ return [];
158
+ }
159
+ throw error;
160
+ }
161
+ },
162
+ async index(doc) {
163
+ const meiliClient = getClient();
164
+ const indexName = buildIndexName(doc.tenantId);
165
+ await ensureIndex(indexName);
166
+ const document = await prepareDocument(doc);
167
+ const index = meiliClient.index(indexName);
168
+ await index.addDocuments([document], { primaryKey: "_id" });
169
+ },
170
+ async delete(recordId, tenantId) {
171
+ const meiliClient = getClient();
172
+ const indexName = buildIndexName(tenantId);
173
+ try {
174
+ const index = meiliClient.index(indexName);
175
+ await index.deleteDocument(recordId);
176
+ } catch (error) {
177
+ const meilisearchError = error;
178
+ if (meilisearchError.code === "index_not_found") {
179
+ return;
180
+ }
181
+ throw error;
182
+ }
183
+ },
184
+ async bulkIndex(docs) {
185
+ if (docs.length === 0) return;
186
+ const byTenant = /* @__PURE__ */ new Map();
187
+ for (const doc of docs) {
188
+ const list = byTenant.get(doc.tenantId) ?? [];
189
+ list.push(doc);
190
+ byTenant.set(doc.tenantId, list);
191
+ }
192
+ const meiliClient = getClient();
193
+ for (const [tenantId, tenantDocs] of byTenant) {
194
+ const indexName = buildIndexName(tenantId);
195
+ await ensureIndex(indexName);
196
+ const documents = await Promise.all(tenantDocs.map(prepareDocument));
197
+ const index = meiliClient.index(indexName);
198
+ await index.addDocuments(documents, { primaryKey: "_id" });
199
+ }
200
+ },
201
+ async purge(entityId, tenantId) {
202
+ const meiliClient = getClient();
203
+ const indexName = buildIndexName(tenantId);
204
+ try {
205
+ const index = meiliClient.index(indexName);
206
+ await index.deleteDocuments({
207
+ filter: `_entityId = "${entityId}"`
208
+ });
209
+ } catch (error) {
210
+ const meilisearchError = error;
211
+ if (meilisearchError.code === "index_not_found") {
212
+ return;
213
+ }
214
+ throw error;
215
+ }
216
+ },
217
+ async clearIndex(tenantId) {
218
+ const meiliClient = getClient();
219
+ const indexName = buildIndexName(tenantId);
220
+ try {
221
+ const index = meiliClient.index(indexName);
222
+ await index.deleteAllDocuments();
223
+ } catch (error) {
224
+ const meilisearchError = error;
225
+ if (meilisearchError.code === "index_not_found") {
226
+ return;
227
+ }
228
+ throw error;
229
+ }
230
+ },
231
+ async recreateIndex(tenantId) {
232
+ const meiliClient = getClient();
233
+ const indexName = buildIndexName(tenantId);
234
+ initializedIndexes.delete(indexName);
235
+ try {
236
+ await meiliClient.deleteIndex(indexName);
237
+ } catch (error) {
238
+ const meilisearchError = error;
239
+ if (meilisearchError.code !== "index_not_found") {
240
+ throw error;
241
+ }
242
+ }
243
+ await ensureIndex(indexName);
244
+ },
245
+ async getDocuments(ids, tenantId) {
246
+ const result = /* @__PURE__ */ new Map();
247
+ if (ids.length === 0) return result;
248
+ const meiliClient = getClient();
249
+ const indexName = buildIndexName(tenantId);
250
+ try {
251
+ const index = meiliClient.index(indexName);
252
+ const recordIds = ids.map((id) => id.recordId);
253
+ const documents = await index.getDocuments({
254
+ filter: `_id IN [${recordIds.map((id) => `"${id}"`).join(", ")}]`,
255
+ limit: recordIds.length
256
+ });
257
+ for (const doc of documents.results) {
258
+ const hit = doc;
259
+ const key = `${hit._entityId}:${hit._id}`;
260
+ result.set(key, {
261
+ recordId: hit._id,
262
+ entityId: hit._entityId,
263
+ score: 0,
264
+ presenter: hit._presenter,
265
+ url: hit._url,
266
+ links: hit._links
267
+ });
268
+ }
269
+ } catch {
270
+ }
271
+ return result;
272
+ },
273
+ async getIndexStats(tenantId) {
274
+ const meiliClient = getClient();
275
+ const indexName = buildIndexName(tenantId);
276
+ try {
277
+ const index = meiliClient.index(indexName);
278
+ const stats = await index.getStats();
279
+ return {
280
+ numberOfDocuments: stats.numberOfDocuments,
281
+ isIndexing: stats.isIndexing,
282
+ fieldDistribution: stats.fieldDistribution
283
+ };
284
+ } catch (error) {
285
+ const meilisearchError = error;
286
+ if (meilisearchError.code === "index_not_found") {
287
+ return null;
288
+ }
289
+ throw error;
290
+ }
291
+ },
292
+ async getEntityCounts(tenantId) {
293
+ const meiliClient = getClient();
294
+ const indexName = buildIndexName(tenantId);
295
+ try {
296
+ const index = meiliClient.index(indexName);
297
+ const searchResult = await index.search("", {
298
+ limit: 0,
299
+ facets: ["_entityId"]
300
+ });
301
+ const facetDistribution = searchResult.facetDistribution?._entityId;
302
+ if (!facetDistribution) {
303
+ return {};
304
+ }
305
+ return facetDistribution;
306
+ } catch (error) {
307
+ const meilisearchError = error;
308
+ if (meilisearchError.code === "index_not_found") {
309
+ return null;
310
+ }
311
+ throw error;
312
+ }
313
+ }
314
+ };
315
+ return driver;
316
+ }
317
+ export {
318
+ createMeilisearchDriver
319
+ };
320
+ //# sourceMappingURL=index.js.map