@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
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
|
+
import type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
5
|
+
import {
|
|
6
|
+
type VectorModuleConfig,
|
|
7
|
+
type VectorEntityConfig,
|
|
8
|
+
type VectorQueryRequest,
|
|
9
|
+
type VectorSearchHit,
|
|
10
|
+
type VectorIndexSource,
|
|
11
|
+
type VectorDriverId,
|
|
12
|
+
type VectorLinkDescriptor,
|
|
13
|
+
type VectorResultPresenter,
|
|
14
|
+
type VectorIndexEntry,
|
|
15
|
+
} from '../types'
|
|
16
|
+
import type { VectorDriver } from '../types'
|
|
17
|
+
import { computeChecksum } from './checksum'
|
|
18
|
+
import { EmbeddingService } from './embedding'
|
|
19
|
+
import { logVectorOperation } from '../lib/vector-logs'
|
|
20
|
+
import { searchDebug, searchDebugWarn } from '../../lib/debug'
|
|
21
|
+
|
|
22
|
+
type ContainerResolver = () => unknown
|
|
23
|
+
const VECTOR_ENTRY_ENCRYPTION_ENTITY_ID = 'vector:vector_search'
|
|
24
|
+
|
|
25
|
+
const ENRICHMENT_FIELD_HINTS: Record<EntityId, string[]> = {
|
|
26
|
+
'customers:customer_entity': [
|
|
27
|
+
'id',
|
|
28
|
+
'organization_id',
|
|
29
|
+
'tenant_id',
|
|
30
|
+
'display_name',
|
|
31
|
+
'description',
|
|
32
|
+
'status',
|
|
33
|
+
'lifecycle_stage',
|
|
34
|
+
'primary_email',
|
|
35
|
+
'primary_phone',
|
|
36
|
+
'kind',
|
|
37
|
+
'customer_kind',
|
|
38
|
+
],
|
|
39
|
+
'customers:customer_comment': ['id', 'organization_id', 'tenant_id', 'entity_id', 'body', 'appearance_icon', 'appearance_color'],
|
|
40
|
+
'customers:customer_activity': ['id', 'organization_id', 'tenant_id', 'entity_id', 'activity_type', 'subject', 'body', 'deal_id'],
|
|
41
|
+
'customers:customer_deal': ['id', 'organization_id', 'tenant_id', 'title', 'pipeline_stage', 'status', 'value_amount', 'value_currency'],
|
|
42
|
+
'customers:customer_todo_link': ['id', 'organization_id', 'tenant_id', 'entity_id', 'todo_id', 'todo_source'],
|
|
43
|
+
'customers:customer_person_profile': [
|
|
44
|
+
'id',
|
|
45
|
+
'organization_id',
|
|
46
|
+
'tenant_id',
|
|
47
|
+
'entity_id',
|
|
48
|
+
'first_name',
|
|
49
|
+
'last_name',
|
|
50
|
+
'preferred_name',
|
|
51
|
+
'job_title',
|
|
52
|
+
'department',
|
|
53
|
+
],
|
|
54
|
+
'customers:customer_company_profile': [
|
|
55
|
+
'id',
|
|
56
|
+
'organization_id',
|
|
57
|
+
'tenant_id',
|
|
58
|
+
'entity_id',
|
|
59
|
+
'brand_name',
|
|
60
|
+
'legal_name',
|
|
61
|
+
'domain',
|
|
62
|
+
'industry',
|
|
63
|
+
'size_bucket',
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type VectorIndexServiceOptions = {
|
|
68
|
+
drivers: VectorDriver[]
|
|
69
|
+
embeddingService: EmbeddingService
|
|
70
|
+
queryEngine: QueryEngine
|
|
71
|
+
moduleConfigs: VectorModuleConfig[]
|
|
72
|
+
defaultDriverId?: VectorDriverId
|
|
73
|
+
containerResolver?: ContainerResolver
|
|
74
|
+
eventBus?: {
|
|
75
|
+
emitEvent(event: string, payload: any, options?: any): Promise<void>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type IndexRecordArgs = {
|
|
80
|
+
entityId: EntityId
|
|
81
|
+
recordId: string
|
|
82
|
+
tenantId: string
|
|
83
|
+
organizationId?: string | null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type DeleteRecordArgs = {
|
|
87
|
+
entityId: EntityId
|
|
88
|
+
recordId: string
|
|
89
|
+
tenantId: string
|
|
90
|
+
organizationId?: string | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type VectorIndexOperationResult = {
|
|
94
|
+
action: 'indexed' | 'deleted' | 'skipped'
|
|
95
|
+
created?: boolean
|
|
96
|
+
existed?: boolean
|
|
97
|
+
tenantId: string
|
|
98
|
+
organizationId: string | null
|
|
99
|
+
reason?: 'unsupported' | 'missing_record' | 'checksum_match'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class VectorIndexService {
|
|
103
|
+
private readonly driverMap = new Map<VectorDriverId, VectorDriver>()
|
|
104
|
+
private readonly entityConfig = new Map<EntityId, { config: VectorEntityConfig; driverId: VectorDriverId }>()
|
|
105
|
+
private readonly defaultDriverId: VectorDriverId
|
|
106
|
+
|
|
107
|
+
constructor(private readonly opts: VectorIndexServiceOptions) {
|
|
108
|
+
for (const driver of opts.drivers) {
|
|
109
|
+
this.driverMap.set(driver.id, driver)
|
|
110
|
+
}
|
|
111
|
+
this.defaultDriverId = opts.defaultDriverId ?? 'pgvector'
|
|
112
|
+
for (const moduleConfig of opts.moduleConfigs) {
|
|
113
|
+
const driverId = moduleConfig.defaultDriverId ?? this.defaultDriverId
|
|
114
|
+
for (const entity of moduleConfig.entities ?? []) {
|
|
115
|
+
if (!entity?.entityId) continue
|
|
116
|
+
if (entity.enabled === false) continue
|
|
117
|
+
const targetDriver = entity.driverId ?? driverId
|
|
118
|
+
this.entityConfig.set(entity.entityId, { config: entity, driverId: targetDriver })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private resolveEncryptionService(): TenantDataEncryptionService | null {
|
|
124
|
+
if (!this.opts.containerResolver) return null
|
|
125
|
+
try {
|
|
126
|
+
const container = this.opts.containerResolver() as any
|
|
127
|
+
if (!container || typeof container.resolve !== 'function') return null
|
|
128
|
+
return container.resolve('tenantEncryptionService') as TenantDataEncryptionService
|
|
129
|
+
} catch {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async encryptResultFields(args: {
|
|
135
|
+
tenantId: string
|
|
136
|
+
organizationId: string | null
|
|
137
|
+
resultTitle: string
|
|
138
|
+
resultSubtitle: string | null
|
|
139
|
+
resultIcon: string | null
|
|
140
|
+
resultSnapshot: string | null
|
|
141
|
+
primaryLinkHref: string | null
|
|
142
|
+
primaryLinkLabel: string | null
|
|
143
|
+
links: VectorLinkDescriptor[] | string | null
|
|
144
|
+
payload: Record<string, unknown> | string | null
|
|
145
|
+
}): Promise<{
|
|
146
|
+
resultTitle: string
|
|
147
|
+
resultSubtitle: string | null
|
|
148
|
+
resultIcon: string | null
|
|
149
|
+
resultSnapshot: string | null
|
|
150
|
+
primaryLinkHref: string | null
|
|
151
|
+
primaryLinkLabel: string | null
|
|
152
|
+
links: VectorLinkDescriptor[] | string | null
|
|
153
|
+
payload: Record<string, unknown> | string | null
|
|
154
|
+
}> {
|
|
155
|
+
const service = this.resolveEncryptionService()
|
|
156
|
+
if (!service || !service.isEnabled?.()) {
|
|
157
|
+
return {
|
|
158
|
+
resultTitle: args.resultTitle,
|
|
159
|
+
resultSubtitle: args.resultSubtitle,
|
|
160
|
+
resultIcon: args.resultIcon,
|
|
161
|
+
resultSnapshot: args.resultSnapshot,
|
|
162
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
163
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
164
|
+
links: args.links,
|
|
165
|
+
payload: args.payload,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const encrypted = await service.encryptEntityPayload(
|
|
170
|
+
VECTOR_ENTRY_ENCRYPTION_ENTITY_ID,
|
|
171
|
+
{
|
|
172
|
+
resultTitle: args.resultTitle,
|
|
173
|
+
resultSubtitle: args.resultSubtitle,
|
|
174
|
+
resultIcon: args.resultIcon,
|
|
175
|
+
resultSnapshot: args.resultSnapshot,
|
|
176
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
177
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
178
|
+
links: args.links,
|
|
179
|
+
payload: args.payload,
|
|
180
|
+
},
|
|
181
|
+
args.tenantId,
|
|
182
|
+
args.organizationId,
|
|
183
|
+
)
|
|
184
|
+
return {
|
|
185
|
+
resultTitle: String((encrypted as any).resultTitle ?? args.resultTitle),
|
|
186
|
+
resultSubtitle: ((encrypted as any).resultSubtitle ?? args.resultSubtitle) as any,
|
|
187
|
+
resultIcon: ((encrypted as any).resultIcon ?? args.resultIcon) as any,
|
|
188
|
+
resultSnapshot: ((encrypted as any).resultSnapshot ?? args.resultSnapshot) as any,
|
|
189
|
+
primaryLinkHref: ((encrypted as any).primaryLinkHref ?? args.primaryLinkHref) as any,
|
|
190
|
+
primaryLinkLabel: ((encrypted as any).primaryLinkLabel ?? args.primaryLinkLabel) as any,
|
|
191
|
+
links: ((encrypted as any).links ?? args.links) as any,
|
|
192
|
+
payload: ((encrypted as any).payload ?? args.payload) as any,
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
return {
|
|
196
|
+
resultTitle: args.resultTitle,
|
|
197
|
+
resultSubtitle: args.resultSubtitle,
|
|
198
|
+
resultIcon: args.resultIcon,
|
|
199
|
+
resultSnapshot: args.resultSnapshot,
|
|
200
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
201
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
202
|
+
links: args.links,
|
|
203
|
+
payload: args.payload,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async decryptResultFields(args: {
|
|
209
|
+
tenantId: string
|
|
210
|
+
organizationId: string | null
|
|
211
|
+
resultTitle: string
|
|
212
|
+
resultSubtitle: string | null
|
|
213
|
+
resultIcon: string | null
|
|
214
|
+
resultSnapshot: string | null
|
|
215
|
+
primaryLinkHref: string | null
|
|
216
|
+
primaryLinkLabel: string | null
|
|
217
|
+
links: VectorLinkDescriptor[] | string | null
|
|
218
|
+
payload: Record<string, unknown> | string | null
|
|
219
|
+
}): Promise<{
|
|
220
|
+
resultTitle: string
|
|
221
|
+
resultSubtitle: string | null
|
|
222
|
+
resultIcon: string | null
|
|
223
|
+
resultSnapshot: string | null
|
|
224
|
+
primaryLinkHref: string | null
|
|
225
|
+
primaryLinkLabel: string | null
|
|
226
|
+
links: VectorLinkDescriptor[] | string | null
|
|
227
|
+
payload: Record<string, unknown> | string | null
|
|
228
|
+
}> {
|
|
229
|
+
const service = this.resolveEncryptionService()
|
|
230
|
+
if (!service || !service.isEnabled?.() || typeof service.decryptEntityPayload !== 'function') {
|
|
231
|
+
return {
|
|
232
|
+
resultTitle: args.resultTitle,
|
|
233
|
+
resultSubtitle: args.resultSubtitle,
|
|
234
|
+
resultIcon: args.resultIcon,
|
|
235
|
+
resultSnapshot: args.resultSnapshot,
|
|
236
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
237
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
238
|
+
links: args.links,
|
|
239
|
+
payload: args.payload,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const decrypted = await service.decryptEntityPayload(
|
|
244
|
+
VECTOR_ENTRY_ENCRYPTION_ENTITY_ID,
|
|
245
|
+
{
|
|
246
|
+
resultTitle: args.resultTitle,
|
|
247
|
+
resultSubtitle: args.resultSubtitle,
|
|
248
|
+
resultIcon: args.resultIcon,
|
|
249
|
+
resultSnapshot: args.resultSnapshot,
|
|
250
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
251
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
252
|
+
links: args.links,
|
|
253
|
+
payload: args.payload,
|
|
254
|
+
},
|
|
255
|
+
args.tenantId,
|
|
256
|
+
args.organizationId,
|
|
257
|
+
)
|
|
258
|
+
return {
|
|
259
|
+
resultTitle: String((decrypted as any).resultTitle ?? args.resultTitle),
|
|
260
|
+
resultSubtitle: ((decrypted as any).resultSubtitle ?? args.resultSubtitle) as any,
|
|
261
|
+
resultIcon: ((decrypted as any).resultIcon ?? args.resultIcon) as any,
|
|
262
|
+
resultSnapshot: ((decrypted as any).resultSnapshot ?? args.resultSnapshot) as any,
|
|
263
|
+
primaryLinkHref: ((decrypted as any).primaryLinkHref ?? args.primaryLinkHref) as any,
|
|
264
|
+
primaryLinkLabel: ((decrypted as any).primaryLinkLabel ?? args.primaryLinkLabel) as any,
|
|
265
|
+
links: ((decrypted as any).links ?? args.links) as any,
|
|
266
|
+
payload: ((decrypted as any).payload ?? args.payload) as any,
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
return {
|
|
270
|
+
resultTitle: args.resultTitle,
|
|
271
|
+
resultSubtitle: args.resultSubtitle,
|
|
272
|
+
resultIcon: args.resultIcon,
|
|
273
|
+
resultSnapshot: args.resultSnapshot,
|
|
274
|
+
primaryLinkHref: args.primaryLinkHref,
|
|
275
|
+
primaryLinkLabel: args.primaryLinkLabel,
|
|
276
|
+
links: args.links,
|
|
277
|
+
payload: args.payload,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
listEnabledEntities(): EntityId[] {
|
|
283
|
+
return Array.from(this.entityConfig.keys())
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async ensureDriverReady(entityId?: EntityId): Promise<void> {
|
|
287
|
+
if (entityId) {
|
|
288
|
+
const entry = this.entityConfig.get(entityId)
|
|
289
|
+
if (!entry) return
|
|
290
|
+
const driver = this.getDriver(entry.driverId)
|
|
291
|
+
await driver.ensureReady()
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
const uniqueDrivers = new Set<VectorDriverId>()
|
|
295
|
+
for (const entry of this.entityConfig.values()) {
|
|
296
|
+
uniqueDrivers.add(entry.driverId)
|
|
297
|
+
}
|
|
298
|
+
if (!uniqueDrivers.size) uniqueDrivers.add(this.defaultDriverId)
|
|
299
|
+
await Promise.all(Array.from(uniqueDrivers).map(async (driverId) => {
|
|
300
|
+
try {
|
|
301
|
+
const driver = this.getDriver(driverId)
|
|
302
|
+
await driver.ensureReady()
|
|
303
|
+
} catch (err) {
|
|
304
|
+
searchDebugWarn('vector', 'Failed to ensure driver readiness', { driverId, error: err instanceof Error ? err.message : err })
|
|
305
|
+
}
|
|
306
|
+
}))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private getDriver(driverId: VectorDriverId): VectorDriver {
|
|
310
|
+
const driver = this.driverMap.get(driverId)
|
|
311
|
+
if (!driver) {
|
|
312
|
+
throw new Error(`[vector] Driver ${driverId} is not registered`)
|
|
313
|
+
}
|
|
314
|
+
return driver
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private getEnrichmentFields(entityId: EntityId): string[] | undefined {
|
|
318
|
+
const hints = ENRICHMENT_FIELD_HINTS[entityId]
|
|
319
|
+
if (!hints) return undefined
|
|
320
|
+
const unique = new Set<string>(['id', ...hints])
|
|
321
|
+
return Array.from(unique)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async fetchRecord(entityId: EntityId, recordIds: string[], tenantId: string, organizationId?: string | null) {
|
|
325
|
+
const filters: Record<string, any> = { id: { $in: recordIds } }
|
|
326
|
+
const result = await this.opts.queryEngine.query(entityId, {
|
|
327
|
+
tenantId,
|
|
328
|
+
organizationId: organizationId ?? undefined,
|
|
329
|
+
filters,
|
|
330
|
+
includeCustomFields: true,
|
|
331
|
+
fields: this.getEnrichmentFields(entityId),
|
|
332
|
+
})
|
|
333
|
+
const byId = new Map<string, Record<string, any>>()
|
|
334
|
+
for (const item of result.items) {
|
|
335
|
+
const key = String((item as any).id ?? '')
|
|
336
|
+
if (!key) continue
|
|
337
|
+
byId.set(key, item as Record<string, any>)
|
|
338
|
+
}
|
|
339
|
+
return byId
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private extractRecordPayload(entityId: EntityId, raw: Record<string, any>) {
|
|
343
|
+
const record: Record<string, any> = {}
|
|
344
|
+
const customFields: Record<string, any> = {}
|
|
345
|
+
const multiMap = new Map<string, boolean>()
|
|
346
|
+
|
|
347
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
348
|
+
if (key.startsWith('cf:') && key.endsWith('__is_multi')) {
|
|
349
|
+
const base = key.replace(/__is_multi$/, '')
|
|
350
|
+
multiMap.set(base, Boolean(value))
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (key.startsWith('cf:')) {
|
|
354
|
+
customFields[key.slice(3)] = value
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
record[key] = value
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const [key, isMulti] of multiMap.entries()) {
|
|
361
|
+
const bare = key.slice(3)
|
|
362
|
+
if (bare && customFields[bare] != null && !Array.isArray(customFields[bare]) && isMulti) {
|
|
363
|
+
customFields[bare] = [customFields[bare]]
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (record.entity_id == null && record.entityId == null && entityId.endsWith('_company_profile')) {
|
|
368
|
+
searchDebugWarn('vector.index', 'company profile missing entity id in payload', {
|
|
369
|
+
id: record.id,
|
|
370
|
+
keys: Object.keys(record),
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { record, customFields }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async indexExisting(
|
|
378
|
+
entry: { config: VectorEntityConfig; driverId: VectorDriverId },
|
|
379
|
+
driver: VectorDriver,
|
|
380
|
+
args: IndexRecordArgs,
|
|
381
|
+
raw: Record<string, any>,
|
|
382
|
+
opts: { skipDelete?: boolean } = {},
|
|
383
|
+
): Promise<VectorIndexOperationResult> {
|
|
384
|
+
const scopeOrg = args.organizationId ?? null
|
|
385
|
+
const { record, customFields } = this.extractRecordPayload(args.entityId, raw)
|
|
386
|
+
const resolvedOrgId = scopeOrg ?? (record.organization_id ?? record.organizationId ?? null)
|
|
387
|
+
const source = await this.resolveSource(args.entityId, entry.config, {
|
|
388
|
+
record,
|
|
389
|
+
customFields,
|
|
390
|
+
tenantId: args.tenantId,
|
|
391
|
+
organizationId: resolvedOrgId,
|
|
392
|
+
})
|
|
393
|
+
if (!source) {
|
|
394
|
+
const existing = await driver.getChecksum(args.entityId, args.recordId, args.tenantId)
|
|
395
|
+
if (!opts.skipDelete && existing) {
|
|
396
|
+
await driver.delete(args.entityId, args.recordId, args.tenantId)
|
|
397
|
+
return {
|
|
398
|
+
action: 'deleted',
|
|
399
|
+
existed: true,
|
|
400
|
+
tenantId: args.tenantId,
|
|
401
|
+
organizationId: resolvedOrgId,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
action: 'skipped',
|
|
406
|
+
existed: Boolean(existing),
|
|
407
|
+
tenantId: args.tenantId,
|
|
408
|
+
organizationId: resolvedOrgId,
|
|
409
|
+
reason: 'missing_record',
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const checksumSource = source.checksumSource ?? { record, customFields }
|
|
414
|
+
const checksum = computeChecksum(checksumSource)
|
|
415
|
+
const current = await driver.getChecksum(args.entityId, args.recordId, args.tenantId)
|
|
416
|
+
if (current && current === checksum) {
|
|
417
|
+
return {
|
|
418
|
+
action: 'skipped',
|
|
419
|
+
existed: true,
|
|
420
|
+
tenantId: args.tenantId,
|
|
421
|
+
organizationId: scopeOrg,
|
|
422
|
+
reason: 'checksum_match',
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!this.opts.embeddingService.available) {
|
|
426
|
+
throw new Error('[vector] Embedding service unavailable (missing OPENAI_API_KEY)')
|
|
427
|
+
}
|
|
428
|
+
const embedding = await this.opts.embeddingService.createEmbedding(source.input)
|
|
429
|
+
const presenter = await this.resolvePresenter(entry.config, {
|
|
430
|
+
record,
|
|
431
|
+
customFields,
|
|
432
|
+
tenantId: args.tenantId,
|
|
433
|
+
organizationId: resolvedOrgId,
|
|
434
|
+
}, source.presenter ?? null)
|
|
435
|
+
if (!presenter?.title) {
|
|
436
|
+
searchDebugWarn('vector.index', 'missing presenter title', {
|
|
437
|
+
entityId: args.entityId,
|
|
438
|
+
recordId: args.recordId,
|
|
439
|
+
recordSample: {
|
|
440
|
+
display_name: record.display_name,
|
|
441
|
+
displayName: record.displayName,
|
|
442
|
+
name: record.name,
|
|
443
|
+
title: record.title,
|
|
444
|
+
subject: record.subject,
|
|
445
|
+
kind: record.kind,
|
|
446
|
+
},
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
const links = await this.resolveLinks(entry.config, {
|
|
450
|
+
record,
|
|
451
|
+
customFields,
|
|
452
|
+
tenantId: args.tenantId,
|
|
453
|
+
organizationId: resolvedOrgId,
|
|
454
|
+
}, source.links ?? null)
|
|
455
|
+
const url = await this.resolveUrl(entry.config, {
|
|
456
|
+
record,
|
|
457
|
+
customFields,
|
|
458
|
+
tenantId: args.tenantId,
|
|
459
|
+
organizationId: resolvedOrgId,
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const normalizedPresenter = this.ensurePresenter(presenter, record, customFields, args.recordId, args.entityId)
|
|
463
|
+
const normalizedLinks = Array.isArray(links) && links.length ? links : null
|
|
464
|
+
const snapshot = this.deriveSnapshot(record, customFields)
|
|
465
|
+
const rawResultTitle = this.resolveResultTitle(normalizedPresenter, record, customFields, args.recordId)
|
|
466
|
+
const rawResultSubtitle = normalizedPresenter.subtitle ?? snapshot ?? null
|
|
467
|
+
const rawResultIcon = normalizedPresenter.icon ?? this.mapDefaultIcon(args.entityId)
|
|
468
|
+
const resultBadge = normalizedPresenter.badge ?? null
|
|
469
|
+
const primaryLink = this.resolvePrimaryLink(normalizedLinks, url, rawResultTitle)
|
|
470
|
+
|
|
471
|
+
const encryptedResult = await this.encryptResultFields({
|
|
472
|
+
tenantId: args.tenantId,
|
|
473
|
+
organizationId: resolvedOrgId,
|
|
474
|
+
resultTitle: rawResultTitle,
|
|
475
|
+
resultSubtitle: rawResultSubtitle,
|
|
476
|
+
resultIcon: rawResultIcon ?? null,
|
|
477
|
+
resultSnapshot: snapshot ?? null,
|
|
478
|
+
primaryLinkHref: primaryLink?.href ?? null,
|
|
479
|
+
primaryLinkLabel: primaryLink?.label ?? null,
|
|
480
|
+
links: normalizedLinks,
|
|
481
|
+
payload: source.payload ?? null,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const presenterForStorage: VectorResultPresenter = {
|
|
485
|
+
title: encryptedResult.resultTitle,
|
|
486
|
+
subtitle: encryptedResult.resultSubtitle ?? undefined,
|
|
487
|
+
icon: encryptedResult.resultIcon ?? undefined,
|
|
488
|
+
badge: resultBadge ?? undefined,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
searchDebug('VectorIndexService', 'Storing vector index entry', {
|
|
492
|
+
entityId: args.entityId,
|
|
493
|
+
recordId: args.recordId,
|
|
494
|
+
tenantId: args.tenantId,
|
|
495
|
+
organizationId: resolvedOrgId,
|
|
496
|
+
})
|
|
497
|
+
await driver.upsert({
|
|
498
|
+
driverId: entry.driverId,
|
|
499
|
+
entityId: args.entityId,
|
|
500
|
+
recordId: args.recordId,
|
|
501
|
+
tenantId: args.tenantId,
|
|
502
|
+
organizationId: resolvedOrgId,
|
|
503
|
+
checksum,
|
|
504
|
+
embedding,
|
|
505
|
+
url: url ?? null,
|
|
506
|
+
presenter: presenterForStorage,
|
|
507
|
+
links: (encryptedResult.links as any) ?? normalizedLinks,
|
|
508
|
+
payload: (encryptedResult.payload as any) ?? source.payload ?? null,
|
|
509
|
+
resultTitle: encryptedResult.resultTitle,
|
|
510
|
+
resultSubtitle: encryptedResult.resultSubtitle,
|
|
511
|
+
resultIcon: encryptedResult.resultIcon ?? null,
|
|
512
|
+
resultBadge,
|
|
513
|
+
resultSnapshot: encryptedResult.resultSnapshot ?? snapshot,
|
|
514
|
+
primaryLinkHref: encryptedResult.primaryLinkHref ?? primaryLink?.href ?? null,
|
|
515
|
+
primaryLinkLabel: encryptedResult.primaryLinkLabel ?? null,
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
action: 'indexed',
|
|
520
|
+
created: !current,
|
|
521
|
+
existed: true,
|
|
522
|
+
tenantId: args.tenantId,
|
|
523
|
+
organizationId: resolvedOrgId,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private ensurePresenter(
|
|
528
|
+
presenter: VectorResultPresenter | null,
|
|
529
|
+
record: Record<string, any>,
|
|
530
|
+
customFields: Record<string, any>,
|
|
531
|
+
recordId: string,
|
|
532
|
+
entityId: EntityId,
|
|
533
|
+
): VectorResultPresenter {
|
|
534
|
+
if (presenter?.title) return presenter
|
|
535
|
+
const fallback = this.buildFallbackPresenter(record, customFields, recordId, entityId)
|
|
536
|
+
return fallback
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private resolveResultTitle(
|
|
540
|
+
presenter: VectorResultPresenter,
|
|
541
|
+
record: Record<string, any>,
|
|
542
|
+
customFields: Record<string, any>,
|
|
543
|
+
recordId: string,
|
|
544
|
+
): string {
|
|
545
|
+
const candidate =
|
|
546
|
+
presenter.title ??
|
|
547
|
+
record.display_name ??
|
|
548
|
+
record.displayName ??
|
|
549
|
+
record.name ??
|
|
550
|
+
record.title ??
|
|
551
|
+
record.subject ??
|
|
552
|
+
customFields.title ??
|
|
553
|
+
customFields.name ??
|
|
554
|
+
recordId
|
|
555
|
+
const text = String(candidate).trim()
|
|
556
|
+
return text.length ? text : recordId
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private buildFallbackPresenter(
|
|
560
|
+
record: Record<string, any>,
|
|
561
|
+
customFields: Record<string, any>,
|
|
562
|
+
recordId: string,
|
|
563
|
+
entityId: EntityId,
|
|
564
|
+
): VectorResultPresenter {
|
|
565
|
+
const titleCandidate =
|
|
566
|
+
record.display_name ??
|
|
567
|
+
record.displayName ??
|
|
568
|
+
record.name ??
|
|
569
|
+
record.title ??
|
|
570
|
+
record.subject ??
|
|
571
|
+
recordId
|
|
572
|
+
const subtitleCandidate =
|
|
573
|
+
record.description ??
|
|
574
|
+
record.summary ??
|
|
575
|
+
record.body ??
|
|
576
|
+
customFields.summary ??
|
|
577
|
+
customFields.description ??
|
|
578
|
+
null
|
|
579
|
+
const icon = typeof record.kind === 'string'
|
|
580
|
+
? this.mapEntityIcon(record.kind)
|
|
581
|
+
: this.mapDefaultIcon(entityId)
|
|
582
|
+
return {
|
|
583
|
+
title: String(titleCandidate),
|
|
584
|
+
subtitle: subtitleCandidate ? String(subtitleCandidate) : undefined,
|
|
585
|
+
icon: icon ?? undefined,
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private mapEntityIcon(kind?: string | null): string | null {
|
|
590
|
+
if (!kind) return null
|
|
591
|
+
const normalized = kind.toLowerCase()
|
|
592
|
+
if (normalized === 'person') return 'user'
|
|
593
|
+
if (normalized === 'company' || normalized === 'organization') return 'building'
|
|
594
|
+
return null
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private mapDefaultIcon(entityId: EntityId): string | null {
|
|
598
|
+
if (entityId.startsWith('customers:customer_deal')) return 'briefcase'
|
|
599
|
+
if (entityId.startsWith('customers:customer_comment')) return 'sticky-note'
|
|
600
|
+
if (entityId.startsWith('customers:customer_activity')) return 'bolt'
|
|
601
|
+
if (entityId.startsWith('customers:customer_todo')) return 'check-square'
|
|
602
|
+
return null
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private resolvePrimaryLink(
|
|
606
|
+
links: VectorLinkDescriptor[] | null,
|
|
607
|
+
url: string | null,
|
|
608
|
+
fallbackLabel: string,
|
|
609
|
+
): { href: string; label: string } | null {
|
|
610
|
+
if (links?.length) {
|
|
611
|
+
const primary = links.find((link) => link.kind === 'primary') ?? links[0]
|
|
612
|
+
if (primary?.href) {
|
|
613
|
+
return { href: primary.href, label: primary.label ?? fallbackLabel }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (url) {
|
|
617
|
+
return { href: url, label: fallbackLabel }
|
|
618
|
+
}
|
|
619
|
+
return null
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private deriveSnapshot(
|
|
623
|
+
record: Record<string, any>,
|
|
624
|
+
customFields: Record<string, any>,
|
|
625
|
+
): string | null {
|
|
626
|
+
const candidates = [
|
|
627
|
+
record.summary,
|
|
628
|
+
record.description,
|
|
629
|
+
record.body,
|
|
630
|
+
customFields.summary,
|
|
631
|
+
customFields.description,
|
|
632
|
+
customFields.body,
|
|
633
|
+
]
|
|
634
|
+
for (const value of candidates) {
|
|
635
|
+
if (typeof value === 'string') {
|
|
636
|
+
const trimmed = value.trim()
|
|
637
|
+
if (trimmed.length) return trimmed
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private buildDefaultSource(entityId: EntityId, payload: { record: Record<string, any>; customFields: Record<string, any> }): VectorIndexSource {
|
|
644
|
+
const { record, customFields } = payload
|
|
645
|
+
const lines: string[] = []
|
|
646
|
+
|
|
647
|
+
const pushEntry = (label: string, value: unknown) => {
|
|
648
|
+
if (value === null || value === undefined) return
|
|
649
|
+
if (typeof value === 'string' && value.trim().length === 0) return
|
|
650
|
+
if (typeof value === 'object') {
|
|
651
|
+
lines.push(`${label}: ${JSON.stringify(value)}`)
|
|
652
|
+
} else {
|
|
653
|
+
lines.push(`${label}: ${value}`)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const preferredFields = ['title', 'name', 'displayName', 'summary', 'subject']
|
|
658
|
+
for (const field of preferredFields) {
|
|
659
|
+
if (record[field] != null) pushEntry(field, record[field])
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
for (const [key, value] of Object.entries(record)) {
|
|
663
|
+
if (preferredFields.includes(key)) continue
|
|
664
|
+
if (key === 'id' || key === 'tenantId' || key === 'organizationId' || key === 'createdAt' || key === 'updatedAt') continue
|
|
665
|
+
pushEntry(key, value)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
for (const [key, value] of Object.entries(customFields)) {
|
|
669
|
+
pushEntry(`custom.${key}`, value)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (lines.length === 0) {
|
|
673
|
+
lines.push(`${entityId}#${record.id ?? ''}`)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
input: lines,
|
|
678
|
+
payload: null,
|
|
679
|
+
checksumSource: { record, customFields },
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private async resolveSource(entityId: EntityId, config: VectorEntityConfig, ctx: {
|
|
684
|
+
record: Record<string, any>
|
|
685
|
+
customFields: Record<string, any>
|
|
686
|
+
organizationId?: string | null
|
|
687
|
+
tenantId: string
|
|
688
|
+
}): Promise<VectorIndexSource | null> {
|
|
689
|
+
const baseCtx = {
|
|
690
|
+
record: ctx.record,
|
|
691
|
+
customFields: ctx.customFields,
|
|
692
|
+
organizationId: ctx.organizationId ?? null,
|
|
693
|
+
tenantId: ctx.tenantId,
|
|
694
|
+
queryEngine: this.opts.queryEngine,
|
|
695
|
+
container: this.opts.containerResolver ? this.opts.containerResolver() : undefined,
|
|
696
|
+
}
|
|
697
|
+
if (config.buildSource) {
|
|
698
|
+
const built = await config.buildSource(baseCtx)
|
|
699
|
+
if (built) return built
|
|
700
|
+
return null
|
|
701
|
+
}
|
|
702
|
+
return this.buildDefaultSource(entityId, { record: ctx.record, customFields: ctx.customFields })
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private async resolvePresenter(
|
|
706
|
+
config: VectorEntityConfig,
|
|
707
|
+
ctx: {
|
|
708
|
+
record: Record<string, any>
|
|
709
|
+
customFields: Record<string, any>
|
|
710
|
+
organizationId?: string | null
|
|
711
|
+
tenantId: string
|
|
712
|
+
},
|
|
713
|
+
fallback?: VectorResultPresenter | null,
|
|
714
|
+
): Promise<VectorResultPresenter | null> {
|
|
715
|
+
const baseCtx = {
|
|
716
|
+
record: ctx.record,
|
|
717
|
+
customFields: ctx.customFields,
|
|
718
|
+
organizationId: ctx.organizationId ?? null,
|
|
719
|
+
tenantId: ctx.tenantId,
|
|
720
|
+
queryEngine: this.opts.queryEngine,
|
|
721
|
+
container: this.opts.containerResolver ? this.opts.containerResolver() : undefined,
|
|
722
|
+
}
|
|
723
|
+
if (config.formatResult) {
|
|
724
|
+
const formatted = await config.formatResult(baseCtx)
|
|
725
|
+
if (formatted) return formatted
|
|
726
|
+
}
|
|
727
|
+
if (fallback) return fallback
|
|
728
|
+
const nameLike = ctx.record.displayName || ctx.record.title || ctx.record.name
|
|
729
|
+
if (typeof nameLike === 'string' && nameLike.trim().length > 0) {
|
|
730
|
+
const subtitle = ctx.record.description || ctx.record.summary
|
|
731
|
+
return {
|
|
732
|
+
title: nameLike,
|
|
733
|
+
subtitle: typeof subtitle === 'string' ? subtitle : undefined,
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return null
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private async resolveLinks(
|
|
740
|
+
config: VectorEntityConfig,
|
|
741
|
+
ctx: {
|
|
742
|
+
record: Record<string, any>
|
|
743
|
+
customFields: Record<string, any>
|
|
744
|
+
organizationId?: string | null
|
|
745
|
+
tenantId: string
|
|
746
|
+
},
|
|
747
|
+
fallback?: VectorLinkDescriptor[] | null,
|
|
748
|
+
): Promise<VectorLinkDescriptor[] | null> {
|
|
749
|
+
const baseCtx = {
|
|
750
|
+
record: ctx.record,
|
|
751
|
+
customFields: ctx.customFields,
|
|
752
|
+
organizationId: ctx.organizationId ?? null,
|
|
753
|
+
tenantId: ctx.tenantId,
|
|
754
|
+
queryEngine: this.opts.queryEngine,
|
|
755
|
+
container: this.opts.containerResolver ? this.opts.containerResolver() : undefined,
|
|
756
|
+
}
|
|
757
|
+
if (config.resolveLinks) {
|
|
758
|
+
const resolved = await config.resolveLinks(baseCtx)
|
|
759
|
+
if (resolved?.length) return resolved
|
|
760
|
+
}
|
|
761
|
+
return fallback ?? null
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private async resolveUrl(
|
|
765
|
+
config: VectorEntityConfig,
|
|
766
|
+
ctx: {
|
|
767
|
+
record: Record<string, any>
|
|
768
|
+
customFields: Record<string, any>
|
|
769
|
+
organizationId?: string | null
|
|
770
|
+
tenantId: string
|
|
771
|
+
},
|
|
772
|
+
fallback?: string | null,
|
|
773
|
+
): Promise<string | null> {
|
|
774
|
+
if (config.resolveUrl) {
|
|
775
|
+
const candidate = await config.resolveUrl({
|
|
776
|
+
record: ctx.record,
|
|
777
|
+
customFields: ctx.customFields,
|
|
778
|
+
organizationId: ctx.organizationId ?? null,
|
|
779
|
+
tenantId: ctx.tenantId,
|
|
780
|
+
queryEngine: this.opts.queryEngine,
|
|
781
|
+
container: this.opts.containerResolver ? this.opts.containerResolver() : undefined,
|
|
782
|
+
})
|
|
783
|
+
if (candidate) return candidate
|
|
784
|
+
}
|
|
785
|
+
return fallback ?? null
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async indexRecord(args: IndexRecordArgs): Promise<VectorIndexOperationResult> {
|
|
789
|
+
const entry = this.entityConfig.get(args.entityId)
|
|
790
|
+
if (!entry) {
|
|
791
|
+
return {
|
|
792
|
+
action: 'skipped',
|
|
793
|
+
existed: false,
|
|
794
|
+
tenantId: args.tenantId,
|
|
795
|
+
organizationId: args.organizationId ?? null,
|
|
796
|
+
reason: 'unsupported',
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const driver = this.getDriver(entry.driverId)
|
|
800
|
+
await driver.ensureReady()
|
|
801
|
+
|
|
802
|
+
const records = await this.fetchRecord(args.entityId, [args.recordId], args.tenantId, args.organizationId)
|
|
803
|
+
const raw = records.get(args.recordId)
|
|
804
|
+
if (!raw) {
|
|
805
|
+
const existing = await driver.getChecksum(args.entityId, args.recordId, args.tenantId)
|
|
806
|
+
if (existing) {
|
|
807
|
+
await driver.delete(args.entityId, args.recordId, args.tenantId)
|
|
808
|
+
return {
|
|
809
|
+
action: 'deleted',
|
|
810
|
+
existed: true,
|
|
811
|
+
tenantId: args.tenantId,
|
|
812
|
+
organizationId: args.organizationId ?? null,
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
action: 'skipped',
|
|
817
|
+
existed: false,
|
|
818
|
+
tenantId: args.tenantId,
|
|
819
|
+
organizationId: args.organizationId ?? null,
|
|
820
|
+
reason: 'missing_record',
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return this.indexExisting(entry, driver, args, raw as Record<string, any>)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async deleteRecord(args: DeleteRecordArgs): Promise<VectorIndexOperationResult> {
|
|
827
|
+
const entry = this.entityConfig.get(args.entityId)
|
|
828
|
+
if (!entry) {
|
|
829
|
+
return {
|
|
830
|
+
action: 'skipped',
|
|
831
|
+
existed: false,
|
|
832
|
+
tenantId: args.tenantId,
|
|
833
|
+
organizationId: args.organizationId ?? null,
|
|
834
|
+
reason: 'unsupported',
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const driver = this.getDriver(entry.driverId)
|
|
838
|
+
await driver.ensureReady()
|
|
839
|
+
const scopeOrg = args.organizationId ?? null
|
|
840
|
+
const existing = await driver.getChecksum(args.entityId, args.recordId, args.tenantId)
|
|
841
|
+
if (existing) {
|
|
842
|
+
await driver.delete(args.entityId, args.recordId, args.tenantId)
|
|
843
|
+
return {
|
|
844
|
+
action: 'deleted',
|
|
845
|
+
existed: true,
|
|
846
|
+
tenantId: args.tenantId,
|
|
847
|
+
organizationId: scopeOrg,
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
action: 'skipped',
|
|
852
|
+
existed: false,
|
|
853
|
+
tenantId: args.tenantId,
|
|
854
|
+
organizationId: scopeOrg,
|
|
855
|
+
reason: 'missing_record',
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async reindexEntity(args: { entityId: EntityId; tenantId?: string | null; organizationId?: string | null; purgeFirst?: boolean }): Promise<void> {
|
|
860
|
+
const entry = this.entityConfig.get(args.entityId)
|
|
861
|
+
if (!entry) return
|
|
862
|
+
const driver = this.getDriver(entry.driverId)
|
|
863
|
+
await driver.ensureReady()
|
|
864
|
+
|
|
865
|
+
const shouldPurge = args.purgeFirst === true
|
|
866
|
+
const reindexStartedAt = new Date()
|
|
867
|
+
|
|
868
|
+
if (this.opts.eventBus) {
|
|
869
|
+
if (shouldPurge && driver.purge && args.tenantId) {
|
|
870
|
+
await driver.purge(args.entityId, args.tenantId)
|
|
871
|
+
} else if (shouldPurge && !args.tenantId) {
|
|
872
|
+
searchDebugWarn('vector', 'Skipping purge for multi-tenant reindex (tenant not provided)')
|
|
873
|
+
}
|
|
874
|
+
const payload: Record<string, unknown> = {
|
|
875
|
+
entityType: args.entityId,
|
|
876
|
+
}
|
|
877
|
+
if (shouldPurge) {
|
|
878
|
+
payload.force = true
|
|
879
|
+
payload.resetCoverage = true
|
|
880
|
+
}
|
|
881
|
+
if (args.tenantId !== undefined) payload.tenantId = args.tenantId
|
|
882
|
+
if (args.organizationId !== undefined) payload.organizationId = args.organizationId
|
|
883
|
+
await this.opts.eventBus.emitEvent('query_index.reindex', payload)
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (!args.tenantId) {
|
|
888
|
+
throw new Error('[vector] Reindex without tenantId requires event bus integration')
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (shouldPurge && driver.purge) {
|
|
892
|
+
await driver.purge(args.entityId, args.tenantId)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const pageSize = 50
|
|
896
|
+
let page = 1
|
|
897
|
+
const loggingEm = this.resolveEntityManager()
|
|
898
|
+
for (;;) {
|
|
899
|
+
const result = await this.opts.queryEngine.query(args.entityId, {
|
|
900
|
+
tenantId: args.tenantId,
|
|
901
|
+
organizationId: args.organizationId ?? undefined,
|
|
902
|
+
page: { page, pageSize },
|
|
903
|
+
includeCustomFields: true,
|
|
904
|
+
fields: this.getEnrichmentFields(args.entityId),
|
|
905
|
+
})
|
|
906
|
+
if (!result.items.length) break
|
|
907
|
+
for (const raw of result.items) {
|
|
908
|
+
const recordId = String((raw as any).id ?? '')
|
|
909
|
+
if (!recordId) continue
|
|
910
|
+
const opResult = await this.indexExisting(
|
|
911
|
+
entry,
|
|
912
|
+
driver,
|
|
913
|
+
{
|
|
914
|
+
entityId: args.entityId,
|
|
915
|
+
recordId,
|
|
916
|
+
tenantId: args.tenantId,
|
|
917
|
+
organizationId: args.organizationId ?? null,
|
|
918
|
+
},
|
|
919
|
+
raw as Record<string, any>,
|
|
920
|
+
{ skipDelete: true },
|
|
921
|
+
)
|
|
922
|
+
await logVectorOperation({
|
|
923
|
+
em: loggingEm,
|
|
924
|
+
handler: 'service:vector.reindex',
|
|
925
|
+
entityType: args.entityId,
|
|
926
|
+
recordId,
|
|
927
|
+
result: opResult,
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
if (result.items.length < pageSize) break
|
|
931
|
+
page += 1
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (shouldPurge) {
|
|
935
|
+
await this.removeOrphans({
|
|
936
|
+
entityId: args.entityId,
|
|
937
|
+
tenantId: args.tenantId,
|
|
938
|
+
organizationId: args.organizationId,
|
|
939
|
+
olderThan: reindexStartedAt,
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async reindexAll(args: { tenantId?: string | null; organizationId?: string | null; purgeFirst?: boolean }): Promise<void> {
|
|
945
|
+
for (const entityId of this.listEnabledEntities()) {
|
|
946
|
+
await this.reindexEntity({ entityId, tenantId: args.tenantId, organizationId: args.organizationId ?? null, purgeFirst: args.purgeFirst })
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private resolveEntityManager(): EntityManager | null {
|
|
951
|
+
if (!this.opts.containerResolver) return null
|
|
952
|
+
try {
|
|
953
|
+
const container = this.opts.containerResolver()
|
|
954
|
+
if (!container || typeof (container as any).resolve !== 'function') return null
|
|
955
|
+
const resolver = container as { resolve(name: string): unknown }
|
|
956
|
+
const em = resolver.resolve('em')
|
|
957
|
+
return (em as EntityManager | null) ?? null
|
|
958
|
+
} catch {
|
|
959
|
+
return null
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async purgeIndex(args: { tenantId: string; organizationId?: string | null; entityId?: EntityId | null }): Promise<void> {
|
|
964
|
+
const targets = args.entityId ? [args.entityId] : this.listEnabledEntities()
|
|
965
|
+
if (!targets.length) return
|
|
966
|
+
|
|
967
|
+
const grouped = new Map<VectorDriverId, EntityId[]>()
|
|
968
|
+
for (const entityId of targets) {
|
|
969
|
+
const cfg = this.entityConfig.get(entityId)
|
|
970
|
+
if (!cfg) continue
|
|
971
|
+
const driver = this.getDriver(cfg.driverId)
|
|
972
|
+
if (typeof driver.purge !== 'function') {
|
|
973
|
+
throw new Error(`[vector] Driver ${cfg.driverId} does not support purging entities`)
|
|
974
|
+
}
|
|
975
|
+
if (!grouped.has(cfg.driverId)) grouped.set(cfg.driverId, [])
|
|
976
|
+
grouped.get(cfg.driverId)!.push(entityId)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
for (const [driverId, entityIds] of grouped.entries()) {
|
|
980
|
+
const driver = this.getDriver(driverId)
|
|
981
|
+
await driver.ensureReady()
|
|
982
|
+
for (const entityId of entityIds) {
|
|
983
|
+
await driver.purge!(entityId, args.tenantId)
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async removeOrphans(args: {
|
|
989
|
+
entityId: EntityId
|
|
990
|
+
tenantId?: string | null
|
|
991
|
+
organizationId?: string | null
|
|
992
|
+
olderThan: Date
|
|
993
|
+
}): Promise<number> {
|
|
994
|
+
const entry = this.entityConfig.get(args.entityId)
|
|
995
|
+
if (!entry) return 0
|
|
996
|
+
const driver = this.getDriver(entry.driverId)
|
|
997
|
+
await driver.ensureReady()
|
|
998
|
+
|
|
999
|
+
const driverAny = driver as VectorDriver & {
|
|
1000
|
+
removeOrphans?: (params: {
|
|
1001
|
+
entityId: EntityId
|
|
1002
|
+
tenantId?: string | null
|
|
1003
|
+
organizationId?: string | null
|
|
1004
|
+
olderThan: Date
|
|
1005
|
+
}) => Promise<number | void>
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (typeof driverAny.removeOrphans === 'function') {
|
|
1009
|
+
const deleted = await driverAny.removeOrphans({
|
|
1010
|
+
entityId: args.entityId,
|
|
1011
|
+
tenantId: args.tenantId,
|
|
1012
|
+
organizationId: args.organizationId,
|
|
1013
|
+
olderThan: args.olderThan,
|
|
1014
|
+
})
|
|
1015
|
+
return typeof deleted === 'number' ? deleted : 0
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
searchDebugWarn('vector', 'Driver does not support orphan cleanup', { driverId: entry.driverId })
|
|
1019
|
+
return 0
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async countIndexEntries(args: {
|
|
1023
|
+
tenantId: string
|
|
1024
|
+
organizationId?: string | null
|
|
1025
|
+
entityId?: EntityId
|
|
1026
|
+
driverId?: VectorDriverId
|
|
1027
|
+
}): Promise<number> {
|
|
1028
|
+
if (!args.tenantId) return 0
|
|
1029
|
+
const targetEntity = args.entityId ? this.entityConfig.get(args.entityId) : null
|
|
1030
|
+
if (args.entityId && !targetEntity) {
|
|
1031
|
+
return 0
|
|
1032
|
+
}
|
|
1033
|
+
const driverId =
|
|
1034
|
+
args.driverId ??
|
|
1035
|
+
(targetEntity ? targetEntity.driverId : this.defaultDriverId)
|
|
1036
|
+
const driver = this.getDriver(driverId)
|
|
1037
|
+
await driver.ensureReady()
|
|
1038
|
+
const countParams = {
|
|
1039
|
+
tenantId: args.tenantId,
|
|
1040
|
+
organizationId: args.organizationId,
|
|
1041
|
+
entityId: args.entityId,
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof driver.count === 'function') {
|
|
1044
|
+
try {
|
|
1045
|
+
return await driver.count(countParams)
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
searchDebugWarn('vector', 'Driver count failed, falling back to list', {
|
|
1048
|
+
driverId,
|
|
1049
|
+
error: err instanceof Error ? err.message : err,
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof driver.list === 'function') {
|
|
1054
|
+
const limit = 1000
|
|
1055
|
+
let offset = 0
|
|
1056
|
+
let total = 0
|
|
1057
|
+
for (;;) {
|
|
1058
|
+
const batch = await driver.list({
|
|
1059
|
+
tenantId: countParams.tenantId,
|
|
1060
|
+
organizationId: countParams.organizationId,
|
|
1061
|
+
entityId: countParams.entityId,
|
|
1062
|
+
limit,
|
|
1063
|
+
offset,
|
|
1064
|
+
orderBy: 'created',
|
|
1065
|
+
})
|
|
1066
|
+
const size = batch.length
|
|
1067
|
+
total += size
|
|
1068
|
+
if (size < limit) break
|
|
1069
|
+
offset += limit
|
|
1070
|
+
}
|
|
1071
|
+
return total
|
|
1072
|
+
}
|
|
1073
|
+
searchDebugWarn('vector', 'Driver does not support counting or listing index entries', { driverId })
|
|
1074
|
+
return 0
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async listIndexEntries(args: {
|
|
1078
|
+
tenantId: string
|
|
1079
|
+
organizationId?: string | null
|
|
1080
|
+
entityId?: EntityId
|
|
1081
|
+
limit?: number
|
|
1082
|
+
offset?: number
|
|
1083
|
+
driverId?: VectorDriverId
|
|
1084
|
+
}): Promise<VectorIndexEntry[]> {
|
|
1085
|
+
const targetEntity = args.entityId ? this.entityConfig.get(args.entityId) : null
|
|
1086
|
+
if (args.entityId && !targetEntity) {
|
|
1087
|
+
return []
|
|
1088
|
+
}
|
|
1089
|
+
const driverId =
|
|
1090
|
+
args.driverId ??
|
|
1091
|
+
(targetEntity ? targetEntity.driverId : this.defaultDriverId)
|
|
1092
|
+
const driver = this.getDriver(driverId)
|
|
1093
|
+
if (typeof driver.list !== 'function') {
|
|
1094
|
+
throw new Error(`[vector] Driver ${driverId} does not support listing index entries`)
|
|
1095
|
+
}
|
|
1096
|
+
await driver.ensureReady()
|
|
1097
|
+
const list = await driver.list({
|
|
1098
|
+
tenantId: args.tenantId,
|
|
1099
|
+
organizationId: args.organizationId,
|
|
1100
|
+
entityId: args.entityId,
|
|
1101
|
+
limit: args.limit,
|
|
1102
|
+
offset: args.offset,
|
|
1103
|
+
orderBy: 'updated',
|
|
1104
|
+
})
|
|
1105
|
+
if (!list.length) {
|
|
1106
|
+
return []
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const decrypted = await Promise.all(
|
|
1110
|
+
list.map(async (entry) => {
|
|
1111
|
+
const decryptedResult = await this.decryptResultFields({
|
|
1112
|
+
tenantId: args.tenantId,
|
|
1113
|
+
organizationId: entry.organizationId ?? null,
|
|
1114
|
+
resultTitle: entry.resultTitle,
|
|
1115
|
+
resultSubtitle: entry.resultSubtitle ?? null,
|
|
1116
|
+
resultIcon: entry.resultIcon ?? null,
|
|
1117
|
+
resultSnapshot: entry.resultSnapshot ?? null,
|
|
1118
|
+
primaryLinkHref: entry.primaryLinkHref ?? null,
|
|
1119
|
+
primaryLinkLabel: entry.primaryLinkLabel ?? null,
|
|
1120
|
+
links: (entry.links as any) ?? null,
|
|
1121
|
+
payload: (entry.payload as any) ?? null,
|
|
1122
|
+
})
|
|
1123
|
+
return {
|
|
1124
|
+
...entry,
|
|
1125
|
+
resultTitle: decryptedResult.resultTitle,
|
|
1126
|
+
resultSubtitle: decryptedResult.resultSubtitle,
|
|
1127
|
+
resultIcon: decryptedResult.resultIcon,
|
|
1128
|
+
resultSnapshot: decryptedResult.resultSnapshot,
|
|
1129
|
+
primaryLinkHref: decryptedResult.primaryLinkHref,
|
|
1130
|
+
primaryLinkLabel: decryptedResult.primaryLinkLabel,
|
|
1131
|
+
links: decryptedResult.links as any,
|
|
1132
|
+
payload: decryptedResult.payload as any,
|
|
1133
|
+
metadata: (decryptedResult.payload as any) ?? null,
|
|
1134
|
+
}
|
|
1135
|
+
}),
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
return decrypted.map((entry) => {
|
|
1139
|
+
const presenter = {
|
|
1140
|
+
title: entry.resultTitle,
|
|
1141
|
+
subtitle: entry.resultSubtitle ?? undefined,
|
|
1142
|
+
icon: entry.resultIcon ?? undefined,
|
|
1143
|
+
badge: entry.presenter?.badge ?? entry.resultBadge ?? undefined,
|
|
1144
|
+
}
|
|
1145
|
+
const links = entry.links ?? (entry.primaryLinkHref
|
|
1146
|
+
? [{ href: entry.primaryLinkHref, label: entry.primaryLinkLabel ?? entry.resultTitle, kind: 'primary' as const }]
|
|
1147
|
+
: null)
|
|
1148
|
+
const url = entry.url ?? entry.primaryLinkHref ?? null
|
|
1149
|
+
const metadata = entry.metadata ?? (entry.resultSnapshot ? { snapshot: entry.resultSnapshot } : null)
|
|
1150
|
+
return {
|
|
1151
|
+
...entry,
|
|
1152
|
+
driverId,
|
|
1153
|
+
presenter,
|
|
1154
|
+
links,
|
|
1155
|
+
url,
|
|
1156
|
+
metadata,
|
|
1157
|
+
score: entry.score ?? null,
|
|
1158
|
+
}
|
|
1159
|
+
})
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async search(request: VectorQueryRequest): Promise<VectorSearchHit[]> {
|
|
1163
|
+
const driverId = request.driverId ?? this.defaultDriverId
|
|
1164
|
+
const driver = this.getDriver(driverId)
|
|
1165
|
+
await driver.ensureReady()
|
|
1166
|
+
if (!this.opts.embeddingService.available) {
|
|
1167
|
+
throw new Error('[vector] Embedding service unavailable (missing OPENAI_API_KEY)')
|
|
1168
|
+
}
|
|
1169
|
+
const embedding = await this.opts.embeddingService.createEmbedding(request.query)
|
|
1170
|
+
const hits = await driver.query({
|
|
1171
|
+
vector: embedding,
|
|
1172
|
+
limit: request.limit ?? 10,
|
|
1173
|
+
filter: {
|
|
1174
|
+
tenantId: request.tenantId,
|
|
1175
|
+
organizationId: request.organizationId ?? null,
|
|
1176
|
+
entityIds: undefined,
|
|
1177
|
+
},
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
if (!hits.length) return []
|
|
1181
|
+
|
|
1182
|
+
const decrypted = await Promise.all(
|
|
1183
|
+
hits.map(async (hit) => {
|
|
1184
|
+
const decryptedResult = await this.decryptResultFields({
|
|
1185
|
+
tenantId: request.tenantId,
|
|
1186
|
+
organizationId: hit.organizationId ?? request.organizationId ?? null,
|
|
1187
|
+
resultTitle: hit.resultTitle,
|
|
1188
|
+
resultSubtitle: hit.resultSubtitle ?? null,
|
|
1189
|
+
resultIcon: hit.resultIcon ?? null,
|
|
1190
|
+
resultSnapshot: hit.resultSnapshot ?? null,
|
|
1191
|
+
primaryLinkHref: hit.primaryLinkHref ?? null,
|
|
1192
|
+
primaryLinkLabel: hit.primaryLinkLabel ?? null,
|
|
1193
|
+
links: (hit.links as any) ?? null,
|
|
1194
|
+
payload: (hit.payload as any) ?? null,
|
|
1195
|
+
})
|
|
1196
|
+
return {
|
|
1197
|
+
...hit,
|
|
1198
|
+
resultTitle: decryptedResult.resultTitle,
|
|
1199
|
+
resultSubtitle: decryptedResult.resultSubtitle,
|
|
1200
|
+
resultIcon: decryptedResult.resultIcon,
|
|
1201
|
+
resultSnapshot: decryptedResult.resultSnapshot,
|
|
1202
|
+
primaryLinkHref: decryptedResult.primaryLinkHref,
|
|
1203
|
+
primaryLinkLabel: decryptedResult.primaryLinkLabel,
|
|
1204
|
+
links: decryptedResult.links as any,
|
|
1205
|
+
payload: decryptedResult.payload as any,
|
|
1206
|
+
}
|
|
1207
|
+
}),
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
return decrypted.map((hit) => {
|
|
1211
|
+
const presenter = {
|
|
1212
|
+
title: hit.resultTitle,
|
|
1213
|
+
subtitle: hit.resultSubtitle ?? undefined,
|
|
1214
|
+
icon: hit.resultIcon ?? undefined,
|
|
1215
|
+
badge: hit.presenter?.badge ?? hit.resultBadge ?? undefined,
|
|
1216
|
+
}
|
|
1217
|
+
const links = hit.links ?? (hit.primaryLinkHref
|
|
1218
|
+
? [{ href: hit.primaryLinkHref, label: hit.primaryLinkLabel ?? hit.resultTitle, kind: 'primary' as const }]
|
|
1219
|
+
: null)
|
|
1220
|
+
const url = hit.url ?? hit.primaryLinkHref ?? null
|
|
1221
|
+
const metadata = hit.payload ?? (hit.resultSnapshot ? { snapshot: hit.resultSnapshot } : null)
|
|
1222
|
+
return {
|
|
1223
|
+
entityId: hit.entityId,
|
|
1224
|
+
recordId: hit.recordId,
|
|
1225
|
+
score: hit.score,
|
|
1226
|
+
url,
|
|
1227
|
+
presenter,
|
|
1228
|
+
links,
|
|
1229
|
+
driverId,
|
|
1230
|
+
metadata,
|
|
1231
|
+
}
|
|
1232
|
+
})
|
|
1233
|
+
}
|
|
1234
|
+
}
|