@open-mercato/search 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a

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 (33) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/modules/search/api/openapi.js +2 -0
  3. package/dist/modules/search/api/openapi.js.map +2 -2
  4. package/dist/modules/search/api/settings/vector-store/route.js +34 -0
  5. package/dist/modules/search/api/settings/vector-store/route.js.map +2 -2
  6. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +2 -2
  7. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +14 -2
  8. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +2 -2
  9. package/dist/modules/search/i18n/de.json +2 -0
  10. package/dist/modules/search/i18n/en.json +2 -0
  11. package/dist/modules/search/i18n/es.json +2 -0
  12. package/dist/modules/search/i18n/ko.json +2 -0
  13. package/dist/modules/search/i18n/pl.json +2 -0
  14. package/dist/strategies/vector.strategy.js +3 -1
  15. package/dist/strategies/vector.strategy.js.map +2 -2
  16. package/dist/vector/drivers/pgvector/index.js +55 -1
  17. package/dist/vector/drivers/pgvector/index.js.map +2 -2
  18. package/dist/vector/types.js.map +2 -2
  19. package/package.json +4 -4
  20. package/src/__tests__/pgvector-extension-availability.test.ts +158 -0
  21. package/src/modules/search/api/openapi.ts +2 -0
  22. package/src/modules/search/api/settings/vector-store/route.ts +43 -0
  23. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +2 -0
  24. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +31 -1
  25. package/src/modules/search/frontend/components/sections/__tests__/VectorSearchSection.test.tsx +36 -1
  26. package/src/modules/search/i18n/de.json +2 -0
  27. package/src/modules/search/i18n/en.json +2 -0
  28. package/src/modules/search/i18n/es.json +2 -0
  29. package/src/modules/search/i18n/ko.json +2 -0
  30. package/src/modules/search/i18n/pl.json +2 -0
  31. package/src/strategies/vector.strategy.ts +7 -1
  32. package/src/vector/drivers/pgvector/index.ts +77 -1
  33. package/src/vector/types.ts +14 -0
@@ -214,6 +214,8 @@
214
214
  "search.settings.vector.sectionTitle": "Vektorsuche",
215
215
  "search.settings.vector.setEnvVar": "Umgebungsvariable setzen",
216
216
  "search.settings.vector.store": "Vektorspeicher",
217
+ "search.settings.vector.storeUnavailable": "Vektorspeicher ist nicht verfügbar",
218
+ "search.settings.vector.storeUnavailableHint": "Vektorindizierung und semantische Suche werden übersprungen, bis der Speicher erreichbar ist. Installieren Sie die pgvector-Erweiterung auf Ihrem PostgreSQL-Server; der Status wird etwa einmal pro Minute neu geprüft.",
217
219
  "search.settings.vector.usingTenantConfig": "Dieser Mandant verwendet eigene Embedding-Einstellungen.",
218
220
  "search.settings.vectorDocumentsLabel": "Vektordokumente",
219
221
  "search.settings.vectorNotConfigured": "Vektorsuche ist nicht konfiguriert.",
@@ -214,6 +214,8 @@
214
214
  "search.settings.vector.sectionTitle": "Vector Search",
215
215
  "search.settings.vector.setEnvVar": "Set environment variable",
216
216
  "search.settings.vector.store": "Vector store",
217
+ "search.settings.vector.storeUnavailable": "Vector store is not available",
218
+ "search.settings.vector.storeUnavailableHint": "Vector indexing and semantic search are skipped until the store is reachable. Install the pgvector extension on your PostgreSQL server; the status is rechecked about once a minute.",
217
219
  "search.settings.vector.usingTenantConfig": "This tenant is using its own embedding settings.",
218
220
  "search.settings.vectorDocumentsLabel": "Vector documents",
219
221
  "search.settings.vectorNotConfigured": "Vector search is not configured.",
@@ -214,6 +214,8 @@
214
214
  "search.settings.vector.sectionTitle": "Búsqueda vectorial",
215
215
  "search.settings.vector.setEnvVar": "Configurar variable de entorno",
216
216
  "search.settings.vector.store": "Almacén vectorial",
217
+ "search.settings.vector.storeUnavailable": "El almacén vectorial no está disponible",
218
+ "search.settings.vector.storeUnavailableHint": "La indexación vectorial y la búsqueda semántica se omiten hasta que el almacén esté disponible. Instala la extensión pgvector en tu servidor PostgreSQL; el estado se vuelve a comprobar aproximadamente cada minuto.",
217
219
  "search.settings.vector.usingTenantConfig": "Este inquilino usa su propia configuración de incrustaciones.",
218
220
  "search.settings.vectorDocumentsLabel": "Documentos vectoriales",
219
221
  "search.settings.vectorNotConfigured": "La búsqueda vectorial no está configurada.",
@@ -214,6 +214,8 @@
214
214
  "search.settings.vector.sectionTitle": "벡터 검색",
215
215
  "search.settings.vector.setEnvVar": "환경 변수 설정",
216
216
  "search.settings.vector.store": "벡터 저장소",
217
+ "search.settings.vector.storeUnavailable": "벡터 저장소를 사용할 수 없습니다",
218
+ "search.settings.vector.storeUnavailableHint": "저장소에 연결할 수 있을 때까지 벡터 인덱싱과 의미 기반 검색은 건너뜁니다. PostgreSQL 서버에 pgvector 확장을 설치하세요. 상태는 약 1분마다 다시 확인됩니다.",
217
219
  "search.settings.vector.usingTenantConfig": "이 테넌트는 자체 임베딩 설정을 사용하고 있습니다.",
218
220
  "search.settings.vectorDocumentsLabel": "벡터 문서",
219
221
  "search.settings.vectorNotConfigured": "벡터 검색이 구성되지 않았습니다.",
@@ -214,6 +214,8 @@
214
214
  "search.settings.vector.sectionTitle": "Wyszukiwanie wektorowe",
215
215
  "search.settings.vector.setEnvVar": "Ustaw zmienną środowiskową",
216
216
  "search.settings.vector.store": "Magazyn wektorów",
217
+ "search.settings.vector.storeUnavailable": "Magazyn wektorów jest niedostępny",
218
+ "search.settings.vector.storeUnavailableHint": "Indeksowanie wektorowe i wyszukiwanie semantyczne są pomijane, dopóki magazyn nie będzie dostępny. Zainstaluj rozszerzenie pgvector na serwerze PostgreSQL; status jest sprawdzany ponownie mniej więcej co minutę.",
217
219
  "search.settings.vector.usingTenantConfig": "Ten najemca używa własnych ustawień osadzania.",
218
220
  "search.settings.vectorDocumentsLabel": "Dokumenty wektorowe",
219
221
  "search.settings.vectorNotConfigured": "Wyszukiwanie wektorowe nie jest skonfigurowane.",
@@ -59,7 +59,13 @@ export class VectorSearchStrategy implements SearchStrategy {
59
59
  }
60
60
 
61
61
  async isAvailable(): Promise<boolean> {
62
- return this.embeddingService.available
62
+ if (!this.embeddingService.available) return false
63
+ // A configured embedding provider says nothing about the vector store. When the
64
+ // store cannot serve writes (pgvector extension missing), reporting availability
65
+ // here makes SearchService call ensureReady() for every record and fail the whole
66
+ // index/delete operation. Drivers without a probe keep the previous behavior.
67
+ if (!this.vectorDriver.isHealthy) return true
68
+ return this.vectorDriver.isHealthy()
63
69
  }
64
70
 
65
71
  async ensureReady(): Promise<void> {
@@ -1,5 +1,5 @@
1
1
  import { Pool } from 'pg'
2
- import { searchDebugWarn } from '../../../lib/debug'
2
+ import { searchDebugWarn, searchWarn } from '../../../lib/debug'
3
3
 
4
4
  type PgPoolQueryResult<T> = { rows: T[]; rowCount?: number }
5
5
  type PgPoolClient = {
@@ -22,6 +22,7 @@ import type {
22
22
  VectorDriverRemoveOrphansParams,
23
23
  VectorResultPresenter,
24
24
  VectorLinkDescriptor,
25
+ VectorDriverStatus,
25
26
  } from '../../types'
26
27
 
27
28
  type PgVectorDriverOptions = {
@@ -38,6 +39,16 @@ const DEFAULT_MIGRATIONS_TABLE = 'vector_search_migrations'
38
39
  const DEFAULT_DIMENSION = 1536
39
40
  const DRIVER_ID = 'pgvector' as const
40
41
 
42
+ // `undefined_file` is what Postgres raises for `extension "vector" is not available`
43
+ // (the pgvector shared library is not installed on the server); `feature_not_supported`
44
+ // covers managed providers that refuse the extension outright, and `undefined_object`
45
+ // the case where the extension record is missing. None of these can be resolved by
46
+ // retrying, so the driver reports itself unhealthy instead of failing every record.
47
+ const EXTENSION_MISSING_CODES = new Set(['58P01', '0A000', '42704'])
48
+ const EXTENSION_UNAVAILABLE_REASON = 'extension "vector" is not available on this PostgreSQL server'
49
+ // Re-probe occasionally so installing pgvector recovers without restarting the process.
50
+ const UNAVAILABLE_RECHECK_MS = 60_000
51
+
41
52
  function assertIdentifier(name: string, defaultName: string): string {
42
53
  const candidate = name ?? defaultName
43
54
  if (!candidate || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(candidate)) return defaultName
@@ -102,8 +113,60 @@ export function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDr
102
113
  })()
103
114
 
104
115
  let ready: Promise<void> | null = null
116
+ let status: { value: VectorDriverStatus; at: number } | null = null
117
+ let reportedReason: string | null = null
118
+
119
+ const cacheStatus = (value: VectorDriverStatus): VectorDriverStatus => {
120
+ status = { value, at: Date.now() }
121
+ if (value.available) {
122
+ reportedReason = null
123
+ } else if (value.reason && value.reason !== reportedReason) {
124
+ reportedReason = value.reason
125
+ // Always visible: replacing a per-record error storm with silence would hide the fact
126
+ // that vector indexing has stopped until someone opens Settings > Search.
127
+ searchWarn('vector.pgvector', `vector store unavailable — skipping vector indexing and search (${value.reason})`)
128
+ }
129
+ return value
130
+ }
131
+
132
+ // The operator-facing reason is curated rather than the raw Postgres message, which the
133
+ // settings API surfaces to the browser.
134
+ const markExtensionMissing = () => {
135
+ cacheStatus({ available: false, reason: EXTENSION_UNAVAILABLE_REASON })
136
+ }
137
+
138
+ const readCachedStatus = (): VectorDriverStatus | null => {
139
+ if (!status) return null
140
+ return Date.now() - status.at < UNAVAILABLE_RECHECK_MS ? status.value : null
141
+ }
142
+
143
+ const getStatus = async (): Promise<VectorDriverStatus> => {
144
+ const cached = readCachedStatus()
145
+ if (cached) return cached
146
+ try {
147
+ const res = await pool.query<{ installed: boolean; installable: boolean }>(
148
+ `SELECT
149
+ EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') AS installed,
150
+ EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'vector') AS installable`,
151
+ )
152
+ const row = res.rows[0]
153
+ if (row?.installed || row?.installable) return cacheStatus({ available: true })
154
+ return cacheStatus({ available: false, reason: EXTENSION_UNAVAILABLE_REASON })
155
+ } catch {
156
+ // A probe that cannot run (connection refused, permissions on the catalogs)
157
+ // says nothing about pgvector itself — stay optimistic so genuinely transient
158
+ // database problems keep their existing retry semantics.
159
+ return { available: true }
160
+ }
161
+ }
162
+
163
+ const isHealthy = async (): Promise<boolean> => (await getStatus()).available
105
164
 
106
165
  const ensureReady = async () => {
166
+ const cached = readCachedStatus()
167
+ if (cached && !cached.available) {
168
+ throw new Error(`[vector.pgvector] ${cached.reason ?? 'vector store unavailable'}`)
169
+ }
107
170
  if (!ready) {
108
171
  ready = withClient(pool, async (client) => {
109
172
  const ensureExtension = async (extension: 'pgcrypto' | 'vector') => {
@@ -116,6 +179,9 @@ export function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDr
116
179
  searchDebugWarn('vector.pgvector', `skipping ${extension} extension creation; requires superuser${details}`)
117
180
  return
118
181
  }
182
+ if (extension === 'vector' && pgError?.code && EXTENSION_MISSING_CODES.has(pgError.code)) {
183
+ markExtensionMissing()
184
+ }
119
185
  throw error
120
186
  }
121
187
  }
@@ -219,8 +285,16 @@ export function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDr
219
285
  `INSERT INTO ${migrationsIdent} (id, applied_at) VALUES ($1, now()) ON CONFLICT (id) DO NOTHING`,
220
286
  ['0001_init'],
221
287
  )
288
+ }).then(() => {
289
+ cacheStatus({ available: true })
222
290
  }).catch((err) => {
223
291
  ready = null
292
+ const pgError = err as { code?: string; message?: string }
293
+ // Without the extension the `vector` column type does not exist either, so the
294
+ // table DDL fails even when `CREATE EXTENSION` was skipped rather than raised.
295
+ if (pgError?.code === '42704' && /\bvector\b/.test(pgError.message ?? '')) {
296
+ markExtensionMissing()
297
+ }
224
298
  throw err
225
299
  })
226
300
  }
@@ -633,6 +707,8 @@ export function createPgVectorDriver(opts: PgVectorDriverOptions = {}): VectorDr
633
707
  return {
634
708
  id: 'pgvector',
635
709
  ensureReady,
710
+ isHealthy,
711
+ getStatus,
636
712
  upsert,
637
713
  delete: remove,
638
714
  getChecksum,
@@ -96,9 +96,23 @@ export type VectorDriverRemoveOrphansParams = {
96
96
  olderThan: Date
97
97
  }
98
98
 
99
+ export type VectorDriverStatus = {
100
+ available: boolean
101
+ reason?: string
102
+ }
103
+
99
104
  export interface VectorDriver {
100
105
  readonly id: VectorDriverId
101
106
  ensureReady(): Promise<void>
107
+ /**
108
+ * Cheap, cached probe answering whether the backing store can serve reads and
109
+ * writes at all. Drivers that cannot satisfy their hard requirements (for
110
+ * pgvector: the `vector` extension) MUST report `false` here so callers skip
111
+ * the strategy instead of failing every record. Optional for backward
112
+ * compatibility — an absent implementation is treated as available.
113
+ */
114
+ isHealthy?(): Promise<boolean>
115
+ getStatus?(): Promise<VectorDriverStatus>
102
116
  upsert(doc: VectorDriverDocument): Promise<void>
103
117
  delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void>
104
118
  query(input: VectorDriverQuery): Promise<VectorDriverQueryResult[]>