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