@harperfast/harper 5.2.13 → 5.3.0-beta.1
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/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +58 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +120 -4
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +3436 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +440 -98
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +56 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +381 -32
- package/components/componentPreparationLock.ts +16 -5
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +537 -113
- package/components/operationsValidation.js +98 -3
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +240 -40
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +357 -48
- package/config/harperConfigEnvVars.ts +170 -27
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +33 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +61 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +91 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +235 -17
- package/dist/components/Application.js +3038 -238
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -0
- package/dist/components/OptionsWatcher.js +440 -99
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +52 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +313 -24
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +476 -113
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +97 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +9 -1
- package/dist/config/RootConfigWatcher.js +228 -37
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +11 -2
- package/dist/config/configUtils.js +304 -47
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +55 -0
- package/dist/resources/DatabaseTransaction.d.ts +77 -0
- package/dist/resources/DatabaseTransaction.js +586 -54
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +43 -6
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +49 -5
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +110 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +117 -24
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +28 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -9
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
- package/dist/resources/RocksTransactionLogStore.js +160 -49
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +165 -10
- package/dist/resources/Table.js +1805 -303
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +181 -2
- package/dist/resources/auditStore.js +640 -29
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +157 -14
- package/dist/resources/databases.js +1462 -325
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +52 -16
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +123 -0
- package/dist/resources/recordLock.js +315 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +157 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +283 -37
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +77 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +106 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +16 -1
- package/dist/server/http.js +122 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +27 -0
- package/dist/server/serverHelpers/Headers.js +145 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
- package/dist/server/serverHelpers/contentTypes.js +221 -40
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +178 -29
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +17 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +83 -3
- package/dist/server/threads/manageThreads.js +769 -75
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +277 -31
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +76 -22
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +33 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +18 -0
- package/dist/utility/hdbTerms.js +20 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +286 -30
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +192 -85
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +11 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +41 -0
- package/dist/utility/watcherFallback.js +153 -0
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +205 -75
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +8 -0
- package/json/systemSchema.json +55 -0
- package/npm-shrinkwrap.json +286 -194
- package/package.json +15 -7
- package/resources/DESIGN.md +219 -52
- package/resources/DatabaseTransaction.ts +657 -52
- package/resources/LMDBTransaction.ts +46 -6
- package/resources/PrimaryRocksDatabase.ts +46 -7
- package/resources/RecordEncoder.ts +124 -9
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +114 -22
- package/resources/ResourceInterface.ts +31 -0
- package/resources/RocksIndexStore.ts +30 -9
- package/resources/RocksTransactionLogStore.ts +188 -51
- package/resources/Table.ts +1960 -301
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +647 -32
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +1584 -320
- package/resources/defineTable.ts +18 -2
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +72 -17
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +419 -0
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +152 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +288 -46
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +83 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +194 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +115 -4
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +133 -20
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +135 -0
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +217 -36
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +232 -40
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +15 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +771 -66
- package/server/threads/socketRouter.ts +291 -30
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +73 -22
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +57 -0
- package/utility/hdbTerms.ts +19 -0
- package/utility/logging/harper_logger.ts +278 -26
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +213 -81
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +12 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +148 -0
- package/validation/configValidator.ts +215 -75
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
|
@@ -1,12 +1,39 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
1
2
|
import { cosineDistance, euclideanDistance, dotProductDistance } from './vector.ts';
|
|
2
|
-
import { FLOAT32_OPTIONS } from 'msgpackr';
|
|
3
|
+
import { FLOAT32_OPTIONS, pack, unpack } from 'msgpackr';
|
|
3
4
|
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
4
|
-
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
5
|
+
import { ClientError, ServerError, DerivedIndexLagError } from '../../utility/errors/hdbError.ts';
|
|
5
6
|
import type { Id } from '../../resources/ResourceInterface.ts';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MAX_INDEX_LAG_MILLISECONDS,
|
|
9
|
+
MAX_WAIT_FOR_INDEX_MILLISECONDS,
|
|
10
|
+
type DerivedNativeIndexHost,
|
|
11
|
+
} from './hnswDerivedIndex.ts';
|
|
12
|
+
import { derivedIndexTime, type DerivedIndexReadiness, type DerivedIndexCoverage } from '../derivedIndexRuntime.ts';
|
|
6
13
|
import { SKIP } from '@harperfast/extended-iterable';
|
|
14
|
+
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import {
|
|
17
|
+
getPlaneBinding,
|
|
18
|
+
invalidatePlaneFile as invalidateHnswPlaneFile,
|
|
19
|
+
planeFilePathFor,
|
|
20
|
+
planeStalePathFor,
|
|
21
|
+
PLANE_NO_ID,
|
|
22
|
+
type HnswPlane,
|
|
23
|
+
type PlanePredicate,
|
|
24
|
+
type PlaneSearchHits,
|
|
25
|
+
} from './hnswPlaneBinding.ts';
|
|
7
26
|
|
|
8
27
|
const logger = loggerWithTag('HNSW');
|
|
9
28
|
|
|
29
|
+
/** Element-wise equality of the array-like projections a derived index stores; anything else that failed `===` counts as changed. */
|
|
30
|
+
export function derivedValuesEqual(a: any, b: any): boolean {
|
|
31
|
+
if (a === b) return true;
|
|
32
|
+
if (a == null || b == null || typeof a.length !== 'number' || a.length !== b.length) return false;
|
|
33
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
// int8 scalar quantization of stored graph nodes is ON by default. Each node holds the
|
|
11
38
|
// vector as a compact int8 `bin` plus a per-vector `scale`, roughly a 5x size reduction
|
|
12
39
|
// over float32 and ~10x cheaper to decode (a single typed-array view instead of decoding
|
|
@@ -86,6 +113,10 @@ const ROUTING_EF = 1;
|
|
|
86
113
|
// index's own auto-scaled ceiling so the worst case stays the same order. Schema and per-query ef
|
|
87
114
|
// pins are authoritative cost ceilings; only an automatically scaled index widens from `limit`.
|
|
88
115
|
const LIMIT_EF_MAX = 2 * AUTO_EF_CEILING;
|
|
116
|
+
// Rerank candidates per row a bounded nearest-neighbor query consumes, with a floor: each
|
|
117
|
+
// candidate costs a record load and an exact distance, so the rerank must not scale with ef.
|
|
118
|
+
const RERANK_FACTOR = 4;
|
|
119
|
+
const RERANK_MIN = 32;
|
|
89
120
|
// Auto-scaled construction ef, used only when an index does not explicitly configure efConstruction.
|
|
90
121
|
// At a constant efConstruction, edge quality erodes as the graph grows until true neighbours become
|
|
91
122
|
// unreachable at ANY search ef; the cap bounds per-insert cost. Measurements and policy in
|
|
@@ -105,6 +136,62 @@ function autoScaleEfConstruction(nodeCount: number): number {
|
|
|
105
136
|
// this only has to be short enough that a table growing from empty picks up a larger ef promptly.
|
|
106
137
|
const NODE_COUNT_TTL = 10_000;
|
|
107
138
|
|
|
139
|
+
// Native traversal-plane geometry (DESIGN.md § Native HNSW plane). The layer-0 cap is derived from
|
|
140
|
+
// M/optimizeRouting at creation to cover the JS graph's effective cap (grace overshoot above it
|
|
141
|
+
// truncates by distance); a configuration deriving past this ceiling is refused as ineligible
|
|
142
|
+
// rather than silently truncated. maxNodes is a fixed sparse reservation — pages materialize on
|
|
143
|
+
// write — and ids at or past it are rejected by the crate, which disables the plane.
|
|
144
|
+
const PLANE_LAYER0_CAP_MAX = 1024;
|
|
145
|
+
const PLANE_MAX_NODES = 1 << 24;
|
|
146
|
+
// Default inline primary-key bytes per plane slot (msgpack-encoded); 40 fits UUIDs inside the slot
|
|
147
|
+
// padding. `nativePlaneKeyCap` raises it for tables whose keys are longer.
|
|
148
|
+
const PLANE_KEY_CAP = 40;
|
|
149
|
+
const PLANE_KEY_CAP_MAX = 65535;
|
|
150
|
+
// An existing plane file that cannot be opened is normally another worker mid-create (retry);
|
|
151
|
+
// past this age it is a crashed create and is deleted so the audit-backed runtime can rebuild it.
|
|
152
|
+
const PLANE_STALE_CREATE_MS = 60_000;
|
|
153
|
+
// Retry cadence while another worker holds the create; its header lands shortly after exclusive open.
|
|
154
|
+
const PLANE_ATTACH_RETRY_MS = 250;
|
|
155
|
+
// Marks an error thrown by an app-supplied filter during a plane search: the caller re-raises
|
|
156
|
+
// it as an ordinary query failure instead of disabling the (healthy) plane.
|
|
157
|
+
const NOT_A_PLANE_FAILURE = Symbol('notAPlaneFailure');
|
|
158
|
+
|
|
159
|
+
function numericOption(name: string, value: unknown): number | undefined {
|
|
160
|
+
if (value === undefined || value === null) return undefined;
|
|
161
|
+
if ((typeof value !== 'number' && typeof value !== 'string') || (typeof value === 'string' && value.trim() === ''))
|
|
162
|
+
throw new ClientError(`${name} must be a finite number`);
|
|
163
|
+
const numericValue = Number(value);
|
|
164
|
+
if (!Number.isFinite(numericValue)) throw new ClientError(`${name} must be a finite number`);
|
|
165
|
+
return numericValue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeOptimizeRoutingDeclaration(value: unknown): unknown {
|
|
169
|
+
if (value === true || value === 'true') return 1;
|
|
170
|
+
if (value === false || value === 'false') return 0;
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nativePlaneMaxNodes(options: any): number {
|
|
175
|
+
const maxNodes = numericOption('nativePlaneMaxNodes', options?.nativePlaneMaxNodes) ?? PLANE_MAX_NODES;
|
|
176
|
+
if (!Number.isSafeInteger(maxNodes) || maxNodes < 1 || maxNodes >= PLANE_NO_ID) {
|
|
177
|
+
throw new ClientError('nativePlaneMaxNodes must be a positive integer below 2^32-1');
|
|
178
|
+
}
|
|
179
|
+
return maxNodes;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function nativePlaneKeyCap(options: any): number {
|
|
183
|
+
const keyCap = numericOption('nativePlaneKeyCap', options?.nativePlaneKeyCap) ?? PLANE_KEY_CAP;
|
|
184
|
+
if (!Number.isSafeInteger(keyCap) || keyCap < 8 || keyCap > PLANE_KEY_CAP_MAX) {
|
|
185
|
+
throw new ClientError('nativePlaneKeyCap must be an integer between 8 and 65535');
|
|
186
|
+
}
|
|
187
|
+
return keyCap;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function nativePlaneDefaultDisabled(): boolean {
|
|
191
|
+
const value = process.env.HNSW_NO_NATIVE_DEFAULT;
|
|
192
|
+
return value != null && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
|
|
193
|
+
}
|
|
194
|
+
|
|
108
195
|
class MinHeap {
|
|
109
196
|
private data: Candidate[] = [];
|
|
110
197
|
get size() {
|
|
@@ -162,6 +249,10 @@ function bisectInsert(arr: Candidate[], distance: number): number {
|
|
|
162
249
|
*/
|
|
163
250
|
const ENTRY_POINT = Symbol.for('entryPoint');
|
|
164
251
|
const KEY_PREFIX = Symbol.for('key');
|
|
252
|
+
/** A Map key for a primary key: composite keys by their encoding, since arrays compare by reference. */
|
|
253
|
+
function pendingMappingKey(primaryKey: Id): Id {
|
|
254
|
+
return Array.isArray(primaryKey) ? `\0${pack(primaryKey).toString('latin1')}` : primaryKey;
|
|
255
|
+
}
|
|
165
256
|
const MAX_LEVEL = 10; // should give good high-level skip list performance up to trillions of nodes
|
|
166
257
|
// Visit budget for the post-delete connectivity probe (repairSeveredNeighbors, #1712). Severed
|
|
167
258
|
// islands are small (bounded by the deleted node's neighborhood), so a probe that visits this many
|
|
@@ -191,11 +282,142 @@ type Node = {
|
|
|
191
282
|
*/
|
|
192
283
|
export class HierarchicalNavigableSmallWorld {
|
|
193
284
|
static useObjectStore = true;
|
|
285
|
+
static numericOptions = new Set([
|
|
286
|
+
'M',
|
|
287
|
+
'efConstruction',
|
|
288
|
+
'efConstructionSearch',
|
|
289
|
+
'mL',
|
|
290
|
+
'optimizeRouting',
|
|
291
|
+
'filterExpansion',
|
|
292
|
+
'nativePlaneMaxNodes',
|
|
293
|
+
'nativePlaneKeyCap',
|
|
294
|
+
'maxLagMilliseconds',
|
|
295
|
+
]);
|
|
296
|
+
static truthyStructuralOptions = new Set(['nativePlane']);
|
|
297
|
+
static normalizeOptionValue(name: string, value: unknown): unknown {
|
|
298
|
+
if (name !== 'optimizeRouting') return value;
|
|
299
|
+
if (value === true) return 1;
|
|
300
|
+
if (value === false) return 0;
|
|
301
|
+
if (
|
|
302
|
+
typeof value === 'string' &&
|
|
303
|
+
(value === 'true' || value === 'false' || (value.trim() !== '' && Number(value) === 0))
|
|
304
|
+
)
|
|
305
|
+
// Legacy strings are truthy at runtime; keep them structurally distinct from false/0 so #1357 rebuilds.
|
|
306
|
+
return `legacy:${value}`;
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
static normalizeDeclarationOptions(options: any, persistedOptions?: any): void {
|
|
310
|
+
for (const name of HierarchicalNavigableSmallWorld.numericOptions) {
|
|
311
|
+
if (options[name] === undefined) continue;
|
|
312
|
+
if (persistedOptions && Object.hasOwn(persistedOptions, name) && Object.is(options[name], persistedOptions[name]))
|
|
313
|
+
continue;
|
|
314
|
+
if (
|
|
315
|
+
name === 'optimizeRouting' &&
|
|
316
|
+
typeof persistedOptions?.[name] === 'string' &&
|
|
317
|
+
options[name] !== false &&
|
|
318
|
+
options[name] !== 'false'
|
|
319
|
+
) {
|
|
320
|
+
try {
|
|
321
|
+
const declared = numericOption(name, normalizeOptimizeRoutingDeclaration(options[name]));
|
|
322
|
+
const persisted = numericOption(name, normalizeOptimizeRoutingDeclaration(persistedOptions[name]));
|
|
323
|
+
if (Object.is(declared, persisted)) {
|
|
324
|
+
options[name] = persistedOptions[name];
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
} catch {}
|
|
328
|
+
}
|
|
329
|
+
if (options[name] === null) {
|
|
330
|
+
delete options[name];
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const value = name === 'optimizeRouting' ? normalizeOptimizeRoutingDeclaration(options[name]) : options[name];
|
|
334
|
+
options[name] = numericOption(name, value);
|
|
335
|
+
}
|
|
336
|
+
if (Object.hasOwn(options, 'nativePlaneMaxNodes')) nativePlaneMaxNodes(options);
|
|
337
|
+
if (Object.hasOwn(options, 'nativePlaneKeyCap')) nativePlaneKeyCap(options);
|
|
338
|
+
}
|
|
339
|
+
static normalizeNativePlaneDeclaration(value: unknown): boolean | undefined {
|
|
340
|
+
if (value === undefined || value === null) return undefined;
|
|
341
|
+
if (value === true || value === 'true') return true;
|
|
342
|
+
if (value === false || value === 'false') return false;
|
|
343
|
+
throw new ClientError('nativePlane must be true or false');
|
|
344
|
+
}
|
|
345
|
+
static validateNativePlaneOptions(rootStore: unknown, options: any) {
|
|
346
|
+
if (!(rootStore instanceof RocksDatabase)) {
|
|
347
|
+
throw new ClientError(
|
|
348
|
+
'nativePlane requires the RocksDB storage engine; set nativePlane: false to use the JS index'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const nativeM = numericOption('M', options?.M);
|
|
352
|
+
const nativeEfConstruction = numericOption('efConstruction', options?.efConstruction);
|
|
353
|
+
const nativeML = numericOption('mL', options?.mL);
|
|
354
|
+
const nativeOptimizeRouting = numericOption(
|
|
355
|
+
'optimizeRouting',
|
|
356
|
+
normalizeOptimizeRoutingDeclaration(options?.optimizeRouting)
|
|
357
|
+
);
|
|
358
|
+
if (
|
|
359
|
+
options?.M === null ||
|
|
360
|
+
options?.efConstruction === null ||
|
|
361
|
+
options?.mL === null ||
|
|
362
|
+
options?.optimizeRouting === null ||
|
|
363
|
+
(nativeM !== undefined && nativeM !== 16) ||
|
|
364
|
+
(nativeEfConstruction !== undefined && nativeEfConstruction !== 200) ||
|
|
365
|
+
(nativeML !== undefined && nativeML !== 1 / Math.log(16)) ||
|
|
366
|
+
(nativeOptimizeRouting !== undefined && nativeOptimizeRouting !== 0.5)
|
|
367
|
+
) {
|
|
368
|
+
throw new ClientError(
|
|
369
|
+
'nativePlane requires M=16, efConstruction=200, mL=1/ln(16), and optimizeRouting=0.5; set nativePlane: false to use the JS index'
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const maxNodes = nativePlaneMaxNodes(options);
|
|
373
|
+
const keyCap = nativePlaneKeyCap(options);
|
|
374
|
+
if (options?.quantization === 'none' || options?.distance === 'euclidean' || options?.distance === 'dotProduct') {
|
|
375
|
+
throw new ClientError(
|
|
376
|
+
'nativePlane requires an int8-quantized cosine HNSW index; set nativePlane: false to use the JS index'
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return { nativeM, nativeEfConstruction, nativeML, nativeOptimizeRouting, maxNodes, keyCap };
|
|
380
|
+
}
|
|
381
|
+
static canRunNativePlane(rootStore: unknown, options: any, warnForMissingBinding = true): boolean {
|
|
382
|
+
nativePlaneMaxNodes(options);
|
|
383
|
+
nativePlaneKeyCap(options);
|
|
384
|
+
if (!(rootStore instanceof RocksDatabase) || getPlaneBinding(warnForMissingBinding) == null) return false;
|
|
385
|
+
if (
|
|
386
|
+
options?.M === null ||
|
|
387
|
+
options?.efConstruction === null ||
|
|
388
|
+
options?.mL === null ||
|
|
389
|
+
options?.optimizeRouting === null
|
|
390
|
+
)
|
|
391
|
+
return false;
|
|
392
|
+
const configuredM = numericOption('M', options?.M);
|
|
393
|
+
const configuredEfConstruction = numericOption('efConstruction', options?.efConstruction);
|
|
394
|
+
const configuredML = numericOption('mL', options?.mL);
|
|
395
|
+
const configuredOptimizeRouting = numericOption(
|
|
396
|
+
'optimizeRouting',
|
|
397
|
+
normalizeOptimizeRoutingDeclaration(options?.optimizeRouting)
|
|
398
|
+
);
|
|
399
|
+
const baseEligible =
|
|
400
|
+
options?.quantization !== 'none' &&
|
|
401
|
+
options?.distance !== 'euclidean' &&
|
|
402
|
+
options?.distance !== 'dotProduct' &&
|
|
403
|
+
(configuredM === undefined || configuredM === 16) &&
|
|
404
|
+
(configuredEfConstruction === undefined || configuredEfConstruction === 200) &&
|
|
405
|
+
(configuredML === undefined || configuredML === 1 / Math.log(16)) &&
|
|
406
|
+
(configuredOptimizeRouting === undefined || configuredOptimizeRouting === 0.5);
|
|
407
|
+
if (!baseEligible) return false;
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
static canDefaultToNativePlane(rootStore: unknown, options: any): boolean {
|
|
411
|
+
return (
|
|
412
|
+
!nativePlaneDefaultDisabled() && HierarchicalNavigableSmallWorld.canRunNativePlane(rootStore, options, false)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
194
415
|
// Index options that only affect search, not the stored graph — changing them must not trigger a
|
|
195
416
|
// reindex (databases.ts persists the new value but skips rebuilding). efConstructionSearch is the
|
|
196
417
|
// search-time candidate-list size; the build uses efConstruction/M/distance, which are structural.
|
|
197
|
-
// filterExpansion is the visit-budget multiplier for predicate-aware (filtered) traversal
|
|
198
|
-
|
|
418
|
+
// filterExpansion is the visit-budget multiplier for predicate-aware (filtered) traversal;
|
|
419
|
+
// maxLagMilliseconds controls delivery admission rather than the graph itself.
|
|
420
|
+
static searchOnlyOptions = ['efConstructionSearch', 'filterExpansion', 'maxLagMilliseconds'];
|
|
199
421
|
// Signals to search.ts that this index accepts a per-record predicate in search() and applies it
|
|
200
422
|
// during traversal (predicate-aware / ACORN-style filtering), so companion conditions and RBAC can
|
|
201
423
|
// be pushed down instead of post-filtering an under-filled candidate set (#1241).
|
|
@@ -209,6 +431,10 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
209
431
|
// a value of 1 is extremely aggressive.
|
|
210
432
|
optimizeRouting = 0.5;
|
|
211
433
|
nodesVisitedCount = 0;
|
|
434
|
+
// Test seam: source of randomness for level assignment. Tests that assert exact result-set
|
|
435
|
+
// equality across search strategies replace this with a seeded PRNG so the graph shape is
|
|
436
|
+
// reproducible; greedy routing legitimately diverges on rare unlucky graphs otherwise.
|
|
437
|
+
random: () => number = Math.random;
|
|
212
438
|
// Visit-budget multiplier for predicate-aware traversal (#1241). Under-filled filtered searches
|
|
213
439
|
// stop after the resolved budget ef * filterExpansion visits; automatic search ef contributes at
|
|
214
440
|
// most AUTO_EF_MAX, while explicit schema/query ef remains authoritative. A filter that fills its
|
|
@@ -231,6 +457,33 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
231
457
|
private convertedNodes = new WeakMap<object, any>();
|
|
232
458
|
private nodeCount = 0;
|
|
233
459
|
private nodeCountAt = 0;
|
|
460
|
+
// Native file-primary index. The RocksDB index store holds identity mappings and replay cursors;
|
|
461
|
+
// graph nodes and adjacency exist only in this file.
|
|
462
|
+
// undefined = not yet attached (may retry), null = unavailable or disabled for this process.
|
|
463
|
+
private plane: HnswPlane | null | undefined;
|
|
464
|
+
private planeEligible = false;
|
|
465
|
+
private planeRetryAt = 0;
|
|
466
|
+
private planeDisabledLogged = false;
|
|
467
|
+
private filePrimary = false;
|
|
468
|
+
private nativePlaneMaxNodes = PLANE_MAX_NODES;
|
|
469
|
+
private nativePlaneKeyCap = PLANE_KEY_CAP;
|
|
470
|
+
// Installed by attachDerivedIndexes on every worker: shared readiness of the index and the way
|
|
471
|
+
// to ask its owner for a rebuild. Only the owning worker's runtime ever destroys native state.
|
|
472
|
+
private derivedHost?: DerivedNativeIndexHost;
|
|
473
|
+
// keyed by pendingMappingKey(pk): a composite key's arrays never compare equal by reference
|
|
474
|
+
private pendingDerivedMappings = new Map<
|
|
475
|
+
Id,
|
|
476
|
+
{
|
|
477
|
+
primaryKey: Id;
|
|
478
|
+
id?: number;
|
|
479
|
+
previousId?: number;
|
|
480
|
+
signature?: string;
|
|
481
|
+
version?: number;
|
|
482
|
+
pending?: boolean;
|
|
483
|
+
cleared?: boolean;
|
|
484
|
+
}
|
|
485
|
+
>();
|
|
486
|
+
postCommit?: true;
|
|
234
487
|
constructor(indexStore: any, options: any) {
|
|
235
488
|
this.indexStore = indexStore;
|
|
236
489
|
if (indexStore) {
|
|
@@ -238,10 +491,16 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
238
491
|
// (we would actually like to use float16 if it were available)
|
|
239
492
|
this.indexStore.encoder.useFloat32 = FLOAT32_OPTIONS.ALWAYS;
|
|
240
493
|
}
|
|
494
|
+
const configuredM = options?.M;
|
|
495
|
+
const configuredEfConstruction = options?.efConstruction;
|
|
496
|
+
const configuredEfConstructionSearch = options?.efConstructionSearch;
|
|
497
|
+
const configuredML = options?.mL;
|
|
498
|
+
const configuredOptimizeRouting = options?.optimizeRouting;
|
|
499
|
+
const configuredFilterExpansion = options?.filterExpansion;
|
|
241
500
|
this.int8 = options?.quantization !== 'none';
|
|
242
501
|
// Respect an explicitly-configured ef (efConstruction seeds the search ef too); otherwise auto-scale both.
|
|
243
|
-
this.efSearchConfigured =
|
|
244
|
-
this.efConstructionConfigured =
|
|
502
|
+
this.efSearchConfigured = configuredEfConstructionSearch !== undefined || configuredEfConstruction !== undefined;
|
|
503
|
+
this.efConstructionConfigured = configuredEfConstruction !== undefined;
|
|
245
504
|
this.distance =
|
|
246
505
|
options?.distance === 'euclidean'
|
|
247
506
|
? euclideanDistance
|
|
@@ -250,19 +509,494 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
250
509
|
: cosineDistance;
|
|
251
510
|
if (options) {
|
|
252
511
|
// allow all the HNSW parameters to be configured/tuned
|
|
253
|
-
if (
|
|
254
|
-
this.M =
|
|
512
|
+
if (configuredM !== undefined) {
|
|
513
|
+
this.M = configuredM;
|
|
255
514
|
this.mL = 1 / Math.log(this.M); // recalculate
|
|
256
515
|
}
|
|
257
|
-
if (
|
|
258
|
-
this.efConstruction = this.efConstructionSearch =
|
|
259
|
-
if (
|
|
260
|
-
if (
|
|
261
|
-
if (
|
|
262
|
-
if (
|
|
516
|
+
if (configuredEfConstruction !== undefined)
|
|
517
|
+
this.efConstruction = this.efConstructionSearch = configuredEfConstruction;
|
|
518
|
+
if (configuredEfConstructionSearch !== undefined) this.efConstructionSearch = configuredEfConstructionSearch;
|
|
519
|
+
if (configuredML !== undefined) this.mL = configuredML;
|
|
520
|
+
if (configuredOptimizeRouting !== undefined) this.optimizeRouting = configuredOptimizeRouting;
|
|
521
|
+
if (configuredFilterExpansion !== undefined) this.filterExpansion = configuredFilterExpansion;
|
|
522
|
+
}
|
|
523
|
+
if (options?.nativePlane) {
|
|
524
|
+
const { nativeM, nativeEfConstruction, nativeML, nativeOptimizeRouting, maxNodes, keyCap } =
|
|
525
|
+
HierarchicalNavigableSmallWorld.validateNativePlaneOptions(indexStore?.rootStore, options);
|
|
526
|
+
if (nativeM !== undefined) this.M = nativeM;
|
|
527
|
+
if (nativeML !== undefined) this.mL = nativeML;
|
|
528
|
+
if (nativeOptimizeRouting !== undefined) this.optimizeRouting = nativeOptimizeRouting;
|
|
529
|
+
this.efConstruction = nativeEfConstruction ?? 200;
|
|
530
|
+
this.nativePlaneMaxNodes = maxNodes;
|
|
531
|
+
this.nativePlaneKeyCap = keyCap;
|
|
532
|
+
// The plane stores int8 bins and computes asymmetric cosine only, so the flag is a
|
|
533
|
+
// no-op for float (quantization: "none") and non-cosine indexes; a graph whose derived
|
|
534
|
+
// layer-0 cap exceeds the plane maximum is refused rather than silently truncated.
|
|
535
|
+
this.planeEligible =
|
|
536
|
+
this.int8 && this.distance === cosineDistance && this.planeLayer0Cap() <= PLANE_LAYER0_CAP_MAX;
|
|
537
|
+
if (!this.planeEligible) {
|
|
538
|
+
throw new ClientError(
|
|
539
|
+
'nativePlane requires an int8-quantized cosine HNSW index; set nativePlane: false to use the JS index'
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
this.filePrimary = true;
|
|
543
|
+
this.postCommit = true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Remove derived native state after the option is disabled. */
|
|
548
|
+
cleanupDisabledPlane(): void {
|
|
549
|
+
if (this.planeEligible) return;
|
|
550
|
+
const filePath = this.planeFilePath();
|
|
551
|
+
if (!filePath) return;
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync(filePath)) this.invalidatePlaneFile(filePath, this.plane);
|
|
554
|
+
unlinkSync(filePath);
|
|
555
|
+
logger.info?.('deleted the HNSW plane file of an index no longer using nativePlane');
|
|
556
|
+
} catch (error: any) {
|
|
557
|
+
if (error?.code !== 'ENOENT') {
|
|
558
|
+
// A later re-enable must not adopt a file that missed mutations while disabled.
|
|
559
|
+
logger.warn?.('could not delete the HNSW plane file; marking it stale', error);
|
|
560
|
+
this.invalidatePlaneFile(filePath);
|
|
561
|
+
}
|
|
263
562
|
}
|
|
264
563
|
}
|
|
564
|
+
|
|
565
|
+
/** Absolute path of this index's plane file, or undefined when the store exposes no path. */
|
|
566
|
+
planeFilePath(): string | undefined {
|
|
567
|
+
const storePath = this.indexStore?.path;
|
|
568
|
+
const storeName = this.indexStore?.name;
|
|
569
|
+
if (typeof storePath !== 'string' || typeof storeName !== 'string') return undefined;
|
|
570
|
+
return planeFilePathFor(storePath, storeName);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Open or lazily create the native file. An exclusive create resolves multi-worker races; the
|
|
575
|
+
* derived-index runtime owns population, replay, and publication.
|
|
576
|
+
*/
|
|
577
|
+
private getPlane(dims?: number, dimsFromVector = false): HnswPlane | null {
|
|
578
|
+
if (this.plane !== undefined) return this.plane;
|
|
579
|
+
if (!this.planeEligible) return (this.plane = null);
|
|
580
|
+
const now = Date.now();
|
|
581
|
+
if (now < this.planeRetryAt) return null;
|
|
582
|
+
const Plane = getPlaneBinding();
|
|
583
|
+
if (!Plane) return (this.plane = null); // the loader warned once already
|
|
584
|
+
if (!('keyCap' in Plane.prototype)) {
|
|
585
|
+
if (!this.planeDisabledLogged) {
|
|
586
|
+
this.planeDisabledLogged = true;
|
|
587
|
+
logger.error?.('the installed @harperfast/hnsw predates 0.3.0; the native HNSW index needs 0.3.0 or later');
|
|
588
|
+
}
|
|
589
|
+
return (this.plane = null);
|
|
590
|
+
}
|
|
591
|
+
const filePath = this.planeFilePath();
|
|
592
|
+
if (!filePath) {
|
|
593
|
+
this.disablePlane(new Error('the index store exposes no path to place the plane file next to'));
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
// a tombstone marks a plane a previous unlink could not remove (Windows EBUSY while
|
|
598
|
+
// mapped): the file is stale and must never be opened over a fresh graph
|
|
599
|
+
const stalePath = planeStalePathFor(filePath);
|
|
600
|
+
if (existsSync(stalePath)) {
|
|
601
|
+
try {
|
|
602
|
+
// force: either artifact may already be gone (the documented rollback deletes the
|
|
603
|
+
// plane file by hand), and an ENOENT here disables the plane on every later attach
|
|
604
|
+
rmSync(filePath, { force: true });
|
|
605
|
+
rmSync(stalePath, { force: true });
|
|
606
|
+
} catch {
|
|
607
|
+
this.planeRetryAt = now + NODE_COUNT_TTL;
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (existsSync(filePath)) {
|
|
612
|
+
try {
|
|
613
|
+
// Crash recovery is per-slot inside the crate. The clean flag is advisory;
|
|
614
|
+
// another worker may still be constructing this shared file.
|
|
615
|
+
const opened = Plane.open(filePath);
|
|
616
|
+
if (this.filePrimary && opened.keyCap !== this.nativePlaneKeyCap) {
|
|
617
|
+
// created under another nativePlaneKeyCap: rebuild to the configured layout
|
|
618
|
+
opened.invalidateFile();
|
|
619
|
+
throw new Error(`plane keyCap ${opened.keyCap} differs from the configured ${this.nativePlaneKeyCap}`);
|
|
620
|
+
}
|
|
621
|
+
return (this.plane = opened);
|
|
622
|
+
} catch (openError) {
|
|
623
|
+
if (now - statSync(filePath).mtimeMs <= PLANE_STALE_CREATE_MS) {
|
|
624
|
+
// another worker is between its exclusive create and the header write
|
|
625
|
+
this.planeRetryAt = now + PLANE_ATTACH_RETRY_MS;
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
logger.warn?.('deleting an unopenable HNSW plane file left by an interrupted create', openError);
|
|
629
|
+
unlinkSync(filePath);
|
|
630
|
+
if (this.filePrimary) {
|
|
631
|
+
// The surviving mappings still name node ids from the file just removed.
|
|
632
|
+
// Creating a fresh plane here would resolve them against an empty graph;
|
|
633
|
+
// only reconstruction, which clears the mappings first, is safe.
|
|
634
|
+
this.planeRetryAt = now + PLANE_ATTACH_RETRY_MS;
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (!dims) return null; // open-only call and no file: nothing to attach yet
|
|
640
|
+
// A search target must not pin an empty index's dimensionality: creation is deferred to
|
|
641
|
+
// the first committed vector or to the rebuild scan. Return before the exclusive create
|
|
642
|
+
// rather than creating and unlinking — a concurrent insert that saw the empty file would
|
|
643
|
+
// read it as another worker's in-progress create and 503 for PLANE_STALE_CREATE_MS.
|
|
644
|
+
if (!dimsFromVector) return null;
|
|
645
|
+
let fd: number;
|
|
646
|
+
try {
|
|
647
|
+
fd = openSync(filePath, 'wx');
|
|
648
|
+
} catch {
|
|
649
|
+
// another worker won the create race; its header lands within moments
|
|
650
|
+
this.planeRetryAt = now + PLANE_ATTACH_RETRY_MS;
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
closeSync(fd);
|
|
654
|
+
try {
|
|
655
|
+
return (this.plane = Plane.create(
|
|
656
|
+
filePath,
|
|
657
|
+
dims,
|
|
658
|
+
this.planeLayer0Cap(),
|
|
659
|
+
this.nativePlaneMaxNodes,
|
|
660
|
+
this.nativePlaneKeyCap
|
|
661
|
+
));
|
|
662
|
+
} catch (createError) {
|
|
663
|
+
// Never leave a partial file that a later process could trust as current.
|
|
664
|
+
try {
|
|
665
|
+
unlinkSync(filePath);
|
|
666
|
+
} catch {
|
|
667
|
+
// the disable below already forces the JS path for this process
|
|
668
|
+
}
|
|
669
|
+
this.disablePlane(createError);
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
this.planeRetryAt = now + NODE_COUNT_TTL;
|
|
674
|
+
logger.warn?.('could not attach the HNSW plane file; will retry', error);
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** True only while the owning runtime publishes the index as ready, on whichever worker owns it. */
|
|
680
|
+
private planeSearchReady(plane: HnswPlane): boolean {
|
|
681
|
+
if (plane.invalidated()) {
|
|
682
|
+
this.plane = undefined;
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
return this.derivedReadiness() === 'ready';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private derivedReadiness(): DerivedIndexReadiness['state'] {
|
|
689
|
+
return this.derivedHost?.readiness().state ?? 'unknown';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** The JS graph's effective layer-0 cap for this configuration; sizes the plane's slots. */
|
|
693
|
+
private planeLayer0Cap(): number {
|
|
694
|
+
return this.optimizeRouting ? this.M << 3 : this.M << 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Detach this process from the plane after a failure and ask the index's owner for a rebuild.
|
|
699
|
+
* Only the owner destroys native state (`resetDerivedStorage` under its epoch), so a failure
|
|
700
|
+
* observed here after a peer has already replaced the file cannot take out the replacement.
|
|
701
|
+
*/
|
|
702
|
+
private disablePlane(error: unknown): void {
|
|
703
|
+
this.plane = undefined;
|
|
704
|
+
this.planeRetryAt = Date.now() + PLANE_ATTACH_RETRY_MS;
|
|
705
|
+
if (!this.planeDisabledLogged) {
|
|
706
|
+
this.planeDisabledLogged = true;
|
|
707
|
+
logger.error?.('the HNSW native index failed; requesting a rebuild from its owner', error);
|
|
708
|
+
}
|
|
709
|
+
this.derivedHost?.requestRebuild();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Delete the derived plane state before an audit-backed reconstruction. Path invalidation
|
|
714
|
+
* makes peers stop using an old mapping even when unlink leaves their mmap inode alive.
|
|
715
|
+
*/
|
|
716
|
+
resetDerivedStorage(): void {
|
|
717
|
+
this.pendingDerivedMappings.clear();
|
|
718
|
+
const attached = this.plane;
|
|
719
|
+
this.plane = undefined;
|
|
720
|
+
this.planeRetryAt = 0;
|
|
721
|
+
const filePath = this.planeFilePath();
|
|
722
|
+
if (!filePath) return;
|
|
723
|
+
if (this.filePrimary && existsSync(filePath)) this.invalidatePlaneFile(filePath, attached);
|
|
724
|
+
try {
|
|
725
|
+
unlinkSync(filePath);
|
|
726
|
+
} catch (error: any) {
|
|
727
|
+
if (error?.code !== 'ENOENT') {
|
|
728
|
+
// a stale file that cannot be deleted (e.g. Windows EBUSY while mapped) must not
|
|
729
|
+
// be reopened as if current — mark it so no process ever adopts it
|
|
730
|
+
this.plane = null;
|
|
731
|
+
logger.warn?.('could not delete the HNSW plane file; marking it stale', error);
|
|
732
|
+
this.invalidatePlaneFile(filePath, attached);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** True while any primary-key mapping survives, which is the only proof this index holds nodes. */
|
|
738
|
+
private hasNodeMappings(): boolean {
|
|
739
|
+
for (const { value } of this.indexStore.getRange({})) {
|
|
740
|
+
if (value && typeof value === 'object' && typeof value.id === 'number') return true;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Make an undeletable plane unadoptable before this process releases it. */
|
|
746
|
+
private invalidatePlaneFile(filePath: string, attached?: HnswPlane | null): void {
|
|
747
|
+
try {
|
|
748
|
+
invalidateHnswPlaneFile(filePath, attached);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
logger.warn?.('could not invalidate the stale HNSW plane file', error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Native search over the plane: one NAPI crossing, traversal on the libuv pool, every hit
|
|
756
|
+
* carrying its primary key out of the slot. The predicate adapter runs on this thread's event
|
|
757
|
+
* loop (batched over a ThreadsafeFunction), so this promise must never be awaited by code the
|
|
758
|
+
* predicate itself blocks on; the normal request path awaits it safely.
|
|
759
|
+
*/
|
|
760
|
+
private searchPlane(
|
|
761
|
+
plane: HnswPlane,
|
|
762
|
+
target: number[],
|
|
763
|
+
k: number,
|
|
764
|
+
ef: number,
|
|
765
|
+
filter: ((primaryKey: Id) => boolean) | undefined,
|
|
766
|
+
filterState: FilterState | undefined
|
|
767
|
+
): Promise<any[]> {
|
|
768
|
+
const query = Float32Array.from(target);
|
|
769
|
+
let resultPromise: Promise<PlaneSearchHits>;
|
|
770
|
+
let predicateError: unknown;
|
|
771
|
+
let planeError: unknown;
|
|
772
|
+
if (filter && filterState) {
|
|
773
|
+
const predicate: PlanePredicate = (ids, keys, keyEnds) => {
|
|
774
|
+
const verdicts = new Uint8Array(ids.length);
|
|
775
|
+
if (predicateError !== undefined || planeError !== undefined) return verdicts;
|
|
776
|
+
for (let i = 0; i < ids.length; i++) {
|
|
777
|
+
const start = i === 0 ? 0 : keyEnds[i - 1];
|
|
778
|
+
const end = keyEnds[i];
|
|
779
|
+
if (end === start) continue;
|
|
780
|
+
let primaryKey: Id;
|
|
781
|
+
try {
|
|
782
|
+
primaryKey = unpack(keys.subarray(start, end));
|
|
783
|
+
} catch (error) {
|
|
784
|
+
planeError = error;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
if (this.admit(filter, filterState, primaryKey)) verdicts[i] = 1;
|
|
789
|
+
} catch (error) {
|
|
790
|
+
predicateError = error;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return verdicts;
|
|
795
|
+
};
|
|
796
|
+
resultPromise = plane.searchWithPredicate(query, k, ef, predicate, undefined, filterState.maxVisits);
|
|
797
|
+
} else {
|
|
798
|
+
resultPromise = plane.search(query, k, ef);
|
|
799
|
+
}
|
|
800
|
+
return resultPromise.then((hits) => {
|
|
801
|
+
if (planeError !== undefined) throw planeError;
|
|
802
|
+
if (predicateError !== undefined) {
|
|
803
|
+
// the plane itself is healthy; mark the failure as the application's so the caller
|
|
804
|
+
// re-raises it rather than disabling the plane and retrying
|
|
805
|
+
try {
|
|
806
|
+
(predicateError as any)[NOT_A_PLANE_FAILURE] = true;
|
|
807
|
+
} catch {
|
|
808
|
+
// a frozen/primitive throw still propagates, it just also disables the plane
|
|
809
|
+
}
|
|
810
|
+
throw predicateError;
|
|
811
|
+
}
|
|
812
|
+
const entries: any[] = [];
|
|
813
|
+
const { distances, keys, keyEnds } = hits;
|
|
814
|
+
// a key appears twice only after a crash (see applyDerivedValue); keep the nearer hit
|
|
815
|
+
const seen = new Set<string>();
|
|
816
|
+
for (let i = 0; i < keyEnds.length; i++) {
|
|
817
|
+
const start = i === 0 ? 0 : keyEnds[i - 1];
|
|
818
|
+
const end = keyEnds[i];
|
|
819
|
+
if (end === start) continue;
|
|
820
|
+
const encoded = keys.toString('latin1', start, end);
|
|
821
|
+
if (seen.has(encoded)) continue;
|
|
822
|
+
seen.add(encoded);
|
|
823
|
+
entries.push({ key: unpack(keys.subarray(start, end)), distance: distances[i] });
|
|
824
|
+
}
|
|
825
|
+
// nodesVisited stays 0 here: layer-0 visits happen inside the native traversal
|
|
826
|
+
// (filterEvaluations is still counted by the predicate adapter)
|
|
827
|
+
return withStats(entries, filterState);
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
attachDerivedHost(host: DerivedNativeIndexHost): void {
|
|
832
|
+
this.derivedHost = host;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private queryCoverage(maxLagMilliseconds: number) {
|
|
836
|
+
const coverage = this.derivedHost?.coverage(maxLagMilliseconds);
|
|
837
|
+
if (!coverage || coverage.state === 'unknown') {
|
|
838
|
+
const age = coverage?.lagUpperBoundMilliseconds;
|
|
839
|
+
throw new DerivedIndexLagError(
|
|
840
|
+
`Cannot certify native HNSW index coverage within ${maxLagMilliseconds} ms` +
|
|
841
|
+
(age === undefined ? '; freshness is unknown' : `; last certified ${Math.ceil(age)} ms ago`) +
|
|
842
|
+
'; retry this query'
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
return coverage;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The commit path only validates: a malformed vector is the client's 400 here rather than an
|
|
850
|
+
* unindexable record later. The shared derived-index runtime reads the committed log; nothing
|
|
851
|
+
* is staged on the transaction.
|
|
852
|
+
*/
|
|
853
|
+
prepareCommitted(primaryKey: Id, vector: number[], existingVector: number[]): void {
|
|
854
|
+
// O(dims) on every write to the table, so skip it when the projection did not change — an
|
|
855
|
+
// unchanged vector was validated when it was first written.
|
|
856
|
+
if (derivedValuesEqual(vector, existingVector)) return;
|
|
857
|
+
this.validateVector(primaryKey, vector);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** The runtime's projection guard: what fails here is delivered as unindexable, not applied. */
|
|
861
|
+
assertDerivedValue(vector: unknown, label: string): void {
|
|
862
|
+
this.assertPlaneVector(vector as number[], label);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private validateVector(primaryKey: Id, vector?: number[]): void {
|
|
866
|
+
if (!vector) return;
|
|
867
|
+
this.assertPlaneVector(vector, `Vector for attribute "${String(primaryKey)}"`);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Everything reaching the plane — a committed record's projection or a query target — has to
|
|
872
|
+
* convert to the f32 it stores, and to a representable magnitude. What fails here would instead
|
|
873
|
+
* throw out of `Float32Array.from` or out of the crate, as an error neither the search path nor
|
|
874
|
+
* reconstruction can attribute to the record: a query would unlink a healthy file, and a record
|
|
875
|
+
* would abort every rebuild attempt at the same entry.
|
|
876
|
+
*/
|
|
877
|
+
private assertPlaneVector(vector: number[], label: string): void {
|
|
878
|
+
// A positive integer length, not merely a numeric one: a negative or fractional length skips
|
|
879
|
+
// the component loop and the emptiness check, and reaches Plane.create with it.
|
|
880
|
+
const length = (vector as any)?.length;
|
|
881
|
+
if (!Number.isInteger(length) || length < 1) {
|
|
882
|
+
throw new ClientError(`${label} must be an array of at least one number.`);
|
|
883
|
+
}
|
|
884
|
+
let sumOfSquares = 0;
|
|
885
|
+
for (let i = 0; i < vector.length; i++) {
|
|
886
|
+
const component = vector[i];
|
|
887
|
+
// The type check precedes Math.fround, which throws a TypeError on the BigInt a msgpackr
|
|
888
|
+
// or cbor-x decode produces for a large int64.
|
|
889
|
+
if (typeof component !== 'number' || !Number.isFinite(Math.fround(component))) {
|
|
890
|
+
throw new ClientError(
|
|
891
|
+
`${label} has a component at index ${i} that is not a finite 32-bit float: ${String(component)}.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const asFloat32 = Math.fround(component);
|
|
895
|
+
sumOfSquares += asFloat32 * asFloat32;
|
|
896
|
+
}
|
|
897
|
+
// Components can each be f32-finite while their squares are not, which stores invMag 0 and
|
|
898
|
+
// makes every distance involving that node NaN.
|
|
899
|
+
if (!Number.isFinite(Math.fround(sumOfSquares))) {
|
|
900
|
+
throw new ClientError(`${label} has a magnitude too large to represent in 32-bit floats.`);
|
|
901
|
+
}
|
|
902
|
+
// The plane's dimensionality is fixed at create time by the first committed vector.
|
|
903
|
+
const dims = this.plane?.dims;
|
|
904
|
+
if (dims !== undefined && vector.length !== dims) {
|
|
905
|
+
throw new ClientError(`${label} has ${vector.length} components, but this index stores ${dims}.`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
applyDerivedValue(primaryKey: Id, vector: number[], version?: number): void {
|
|
910
|
+
this.validateVector(primaryKey, vector);
|
|
911
|
+
const safeKey = typeof primaryKey === 'number' ? [KEY_PREFIX, primaryKey] : primaryKey;
|
|
912
|
+
const pendingKey = pendingMappingKey(primaryKey);
|
|
913
|
+
const pendingMapping = this.pendingDerivedMappings.get(pendingKey);
|
|
914
|
+
const storedMapping = pendingMapping ?? this.indexStore.getSync(safeKey);
|
|
915
|
+
const oldNodeId = typeof storedMapping === 'number' ? storedMapping : storedMapping?.id;
|
|
916
|
+
// Neither a plane removal nor an insert is durable before the barrier, so a pending
|
|
917
|
+
// mapping keeps naming the last published node (previousId) for replay to remove.
|
|
918
|
+
const displacedNodeId: number | undefined = storedMapping?.previousId;
|
|
919
|
+
const durableNodeId = storedMapping?.pending ? displacedNodeId : oldNodeId;
|
|
920
|
+
if (storedMapping?.version != null && version != null && storedMapping.version > version) return;
|
|
921
|
+
const nativeVector = vector ? Float32Array.from(vector) : undefined;
|
|
922
|
+
const signature = nativeVector
|
|
923
|
+
? createHash('sha256')
|
|
924
|
+
.update(Buffer.from(nativeVector.buffer, nativeVector.byteOffset, nativeVector.byteLength))
|
|
925
|
+
.digest('base64url')
|
|
926
|
+
: undefined;
|
|
927
|
+
if (
|
|
928
|
+
oldNodeId != null &&
|
|
929
|
+
signature &&
|
|
930
|
+
storedMapping.signature === signature &&
|
|
931
|
+
!pendingMapping &&
|
|
932
|
+
!storedMapping.pending
|
|
933
|
+
) {
|
|
934
|
+
this.indexStore.putSync(safeKey, { id: oldNodeId, signature, version });
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
let plane = vector ? this.getPlane(vector.length, true) : this.getPlane();
|
|
938
|
+
if (plane?.invalidated()) {
|
|
939
|
+
this.plane = undefined;
|
|
940
|
+
plane = vector ? this.getPlane(vector.length, true) : this.getPlane();
|
|
941
|
+
}
|
|
942
|
+
// The pre-commit check above ran before this worker had a plane to compare against, so it
|
|
943
|
+
// cannot have caught a mismatch. Reject before the removal below, or the record loses its
|
|
944
|
+
// old node on the way to failing.
|
|
945
|
+
if (vector && plane && vector.length !== plane.dims) {
|
|
946
|
+
throw new ClientError(
|
|
947
|
+
`Vector for attribute "${String(primaryKey)}" has ${vector.length} components, but this index stores ${plane.dims}.`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
// in process a pending mapping's removals already happened and their ids may be reused
|
|
951
|
+
const removeDisplaced = displacedNodeId != null && !pendingMapping && displacedNodeId !== oldNodeId;
|
|
952
|
+
const removeOld = oldNodeId != null && !(pendingMapping && storedMapping.cleared);
|
|
953
|
+
if (removeDisplaced) plane?.remove(displacedNodeId);
|
|
954
|
+
if (removeOld) plane?.remove(oldNodeId);
|
|
955
|
+
if (!vector) {
|
|
956
|
+
const cleared = { id: oldNodeId, previousId: durableNodeId, version, pending: true, cleared: true };
|
|
957
|
+
if (oldNodeId != null || durableNodeId != null) this.indexStore.putSync(safeKey, cleared);
|
|
958
|
+
else this.indexStore.removeSync(safeKey);
|
|
959
|
+
this.pendingDerivedMappings.set(pendingKey, { ...cleared, primaryKey });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (!plane) throw new ServerError('The native HNSW module is unavailable for a file-primary index', 503);
|
|
963
|
+
const nodeId = plane.insert(nativeVector!, pack(primaryKey));
|
|
964
|
+
const mapping = { id: nodeId, previousId: durableNodeId, signature, version, pending: true };
|
|
965
|
+
this.indexStore.putSync(safeKey, mapping);
|
|
966
|
+
this.pendingDerivedMappings.set(pendingKey, { ...mapping, primaryKey });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async flushDerived(watermark?: number): Promise<void> {
|
|
970
|
+
const plane = this.getPlane();
|
|
971
|
+
if (!plane) {
|
|
972
|
+
if (!getPlaneBinding()) throw new ServerError('The native HNSW module is unavailable', 503);
|
|
973
|
+
return this.publishDerivedMappings();
|
|
974
|
+
}
|
|
975
|
+
await plane.flushAsync(watermark);
|
|
976
|
+
this.publishDerivedMappings();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private publishDerivedMappings(): void {
|
|
980
|
+
for (const { primaryKey, ...mapping } of this.pendingDerivedMappings.values()) {
|
|
981
|
+
const safeKey = typeof primaryKey === 'number' ? [KEY_PREFIX, primaryKey] : primaryKey;
|
|
982
|
+
if (mapping.cleared || mapping.id === undefined) {
|
|
983
|
+
this.indexStore.removeSync(safeKey);
|
|
984
|
+
} else {
|
|
985
|
+
const published = { id: mapping.id, signature: mapping.signature, version: mapping.version };
|
|
986
|
+
this.indexStore.putSync(safeKey, published);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
this.pendingDerivedMappings.clear();
|
|
990
|
+
}
|
|
991
|
+
|
|
265
992
|
index(primaryKey: Id, vector: number[], existingVector?: number[], options: any = {}) {
|
|
993
|
+
if (this.filePrimary) {
|
|
994
|
+
if (options.transaction) return this.prepareCommitted(primaryKey, vector, existingVector);
|
|
995
|
+
// runIndexing invokes custom indexes without a transaction. The shared runtime owns the
|
|
996
|
+
// primary-record rebuild and the log replay; populating here would duplicate native
|
|
997
|
+
// construction and race the owner's generation reset.
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
266
1000
|
// Reject non-finite components before touching the graph. NaN in particular poisons
|
|
267
1001
|
// bisectInsert (arr[mid].distance <= NaN is always false → returns 0, pinning the
|
|
268
1002
|
// candidate to rank 1 of every future search). Infinity causes analogous ordering
|
|
@@ -337,7 +1071,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
337
1071
|
const storedScale = q ? q.scale : undefined;
|
|
338
1072
|
let entryPoint = entryPointId && this.safeGetSync(entryPointId, options);
|
|
339
1073
|
if (entryPoint == null) {
|
|
340
|
-
const level = Math.floor(-Math.log(
|
|
1074
|
+
const level = Math.min(Math.floor(-Math.log(this.random()) * this.mL), MAX_LEVEL);
|
|
341
1075
|
const node = {
|
|
342
1076
|
vector: storedVector,
|
|
343
1077
|
scale: storedScale,
|
|
@@ -358,7 +1092,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
358
1092
|
}
|
|
359
1093
|
|
|
360
1094
|
// Generate random level for this new element
|
|
361
|
-
const level = oldNode.level ?? Math.min(Math.floor(-Math.log(
|
|
1095
|
+
const level = oldNode.level ?? Math.min(Math.floor(-Math.log(this.random()) * this.mL), MAX_LEVEL);
|
|
362
1096
|
let currentLevel = entryPoint.level;
|
|
363
1097
|
if (level > currentLevel) {
|
|
364
1098
|
// if we are at a higher level, make this the new entry point
|
|
@@ -504,18 +1238,15 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
504
1238
|
}
|
|
505
1239
|
|
|
506
1240
|
// Store the new element
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
},
|
|
517
|
-
options
|
|
518
|
-
);
|
|
1241
|
+
const storedNode = {
|
|
1242
|
+
vector: storedVector,
|
|
1243
|
+
scale: storedScale,
|
|
1244
|
+
invMag,
|
|
1245
|
+
level,
|
|
1246
|
+
primaryKey,
|
|
1247
|
+
...connections,
|
|
1248
|
+
};
|
|
1249
|
+
this.indexStore.put(nodeId, storedNode, options);
|
|
519
1250
|
} else {
|
|
520
1251
|
// removal of this node, but first make sure we have a valid entry point
|
|
521
1252
|
if (entryPointId === nodeId) {
|
|
@@ -811,8 +1542,9 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
811
1542
|
}
|
|
812
1543
|
}
|
|
813
1544
|
|
|
814
|
-
/** O(1) node count
|
|
1545
|
+
/** O(1) node count: the plane's id high-water, the shared id counter, or one reverse seek. */
|
|
815
1546
|
private resolveNodeCount(options?: any): number {
|
|
1547
|
+
if (this.filePrimary) return this.getPlane()?.idHighWater() ?? 0;
|
|
816
1548
|
if (this.idIncrementer) return Number(Atomics.load(this.idIncrementer, 0));
|
|
817
1549
|
try {
|
|
818
1550
|
for (const key of this.indexStore.getKeys({
|
|
@@ -1053,12 +1785,14 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1053
1785
|
* This the main entry from Harper's query functionality, where we actually search for an ordered list of nearest
|
|
1054
1786
|
* neighbors, using the provided sort/order definition object and performing the multi-layer skip-list search.
|
|
1055
1787
|
* This returns an iterable of the nearest neighbors to the provided target vector, with nearest ordered first.
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
* @param comparator
|
|
1061
|
-
*
|
|
1788
|
+
*
|
|
1789
|
+
* This is also the contract an index implemented outside this repo has to satisfy, so everything
|
|
1790
|
+
* optional is named: a future capability (a paging cursor, a deadline, a recall target) is a new
|
|
1791
|
+
* field on `options` rather than a positional argument that breaks every existing implementation.
|
|
1792
|
+
* @param searchCondition the vector query: `target`, `comparator`, and the optional `value`,
|
|
1793
|
+
* `descending`, `distance`, `ef` and `filterExpansion` tuning knobs
|
|
1794
|
+
* @param context the query context; its `transaction` is the nested RocksDB transaction reads use
|
|
1795
|
+
* @param options optional, named: `filter` (predicate-aware traversal) and `minResults`
|
|
1062
1796
|
*/
|
|
1063
1797
|
search(
|
|
1064
1798
|
{
|
|
@@ -1069,6 +1803,8 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1069
1803
|
comparator,
|
|
1070
1804
|
ef,
|
|
1071
1805
|
filterExpansion,
|
|
1806
|
+
maxIndexLagMilliseconds = DEFAULT_MAX_INDEX_LAG_MILLISECONDS,
|
|
1807
|
+
waitForIndexMilliseconds = 0,
|
|
1072
1808
|
}: {
|
|
1073
1809
|
target: number[];
|
|
1074
1810
|
value: number;
|
|
@@ -1077,19 +1813,27 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1077
1813
|
comparator: string;
|
|
1078
1814
|
ef?: number;
|
|
1079
1815
|
filterExpansion?: number;
|
|
1816
|
+
maxIndexLagMilliseconds?: number;
|
|
1817
|
+
waitForIndexMilliseconds?: number;
|
|
1080
1818
|
},
|
|
1081
1819
|
context: any,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1820
|
+
{
|
|
1821
|
+
filter,
|
|
1822
|
+
minResults,
|
|
1823
|
+
}: {
|
|
1824
|
+
// Predicate-aware traversal (#1241). When provided, only nodes for which `filter(primaryKey)`
|
|
1825
|
+
// returns true are admitted to the result list at layer 0; routing is unaffected. Composed by
|
|
1826
|
+
// search.ts from companion AND conditions and caller-supplied vector/row filters. Must be
|
|
1827
|
+
// synchronous and side-effect free. JS-API only (never from a REST query string).
|
|
1828
|
+
filter?: (primaryKey: Id) => boolean;
|
|
1829
|
+
// offset + limit for a bounded query. A layer-0 search returns at most `ef` candidates, so a
|
|
1830
|
+
// query asking for more rows than that used to come back short with no error — capped at 512
|
|
1831
|
+
// (AUTO_EF_MAX) however large the limit was. Raising ef to cover the request keeps `limit`
|
|
1832
|
+
// meaningful; the caller pays for what it asked for.
|
|
1833
|
+
minResults?: number;
|
|
1834
|
+
} = {}
|
|
1092
1835
|
) {
|
|
1836
|
+
const waiting = this.filePrimary && waitForIndexMilliseconds > 0;
|
|
1093
1837
|
let limit: number | undefined; // only set for threshold comparators; 0 is a valid threshold (e.g. dotProduct)
|
|
1094
1838
|
let limitInclusive = false; // true for `le`, false for `lt`
|
|
1095
1839
|
switch (comparator) {
|
|
@@ -1116,8 +1860,25 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1116
1860
|
else distanceFunction = this.distance;
|
|
1117
1861
|
if (!target) throw new ClientError('A target vector must be provided for an HNSW query');
|
|
1118
1862
|
if (!Array.isArray(target)) throw new ClientError('The target vector must be an array');
|
|
1863
|
+
if (
|
|
1864
|
+
this.filePrimary &&
|
|
1865
|
+
(typeof maxIndexLagMilliseconds !== 'number' ||
|
|
1866
|
+
!Number.isFinite(maxIndexLagMilliseconds) ||
|
|
1867
|
+
maxIndexLagMilliseconds < 0)
|
|
1868
|
+
)
|
|
1869
|
+
throw new ClientError('maxIndexLagMilliseconds must be a finite nonnegative number');
|
|
1870
|
+
if (
|
|
1871
|
+
this.filePrimary &&
|
|
1872
|
+
(typeof waitForIndexMilliseconds !== 'number' ||
|
|
1873
|
+
!Number.isFinite(waitForIndexMilliseconds) ||
|
|
1874
|
+
waitForIndexMilliseconds < 0 ||
|
|
1875
|
+
waitForIndexMilliseconds > MAX_WAIT_FOR_INDEX_MILLISECONDS)
|
|
1876
|
+
)
|
|
1877
|
+
throw new ClientError(
|
|
1878
|
+
`waitForIndexMilliseconds must be a finite number between 0 and ${MAX_WAIT_FOR_INDEX_MILLISECONDS}`
|
|
1879
|
+
);
|
|
1119
1880
|
|
|
1120
|
-
const
|
|
1881
|
+
const txnOptions = context.transaction; // should have a nested RocksDB transaction
|
|
1121
1882
|
// Resolve search ef: per-query ef wins; else use the schema-pinned value (from either ef option);
|
|
1122
1883
|
// otherwise auto-scale with the graph size so recall holds as the table grows.
|
|
1123
1884
|
let effectiveEf = this.efConstructionSearch;
|
|
@@ -1159,7 +1920,108 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1159
1920
|
filterEvaluations: 0,
|
|
1160
1921
|
}
|
|
1161
1922
|
: undefined;
|
|
1162
|
-
|
|
1923
|
+
const rerankK =
|
|
1924
|
+
comparator === 'sort' && minResults !== undefined
|
|
1925
|
+
? Math.min(effectiveEf, Math.max(RERANK_MIN, minResults * RERANK_FACTOR))
|
|
1926
|
+
: effectiveEf;
|
|
1927
|
+
if (this.filePrimary && distanceFunction !== this.distance) {
|
|
1928
|
+
throw new ClientError('A nativePlane index only supports its configured cosine distance');
|
|
1929
|
+
}
|
|
1930
|
+
const searchNative = (certified?: DerivedIndexCoverage) => {
|
|
1931
|
+
if (this.filePrimary && this.derivedReadiness() !== 'ready') {
|
|
1932
|
+
throw new ServerError(
|
|
1933
|
+
`The native HNSW index is ${this.derivedReadiness() === 'unavailable' ? 'unavailable' : 'rebuilding'}`,
|
|
1934
|
+
503
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
// The plane traverses the index's own metric (cosine — the eligibility requirement), so a
|
|
1938
|
+
// query overriding `distance` has to take the JS path: rescoreResults only corrects the
|
|
1939
|
+
// reported distances of whatever candidates came back, not which candidates the beam kept.
|
|
1940
|
+
if (this.planeEligible && distanceFunction === this.distance) {
|
|
1941
|
+
const plane = this.getPlane(target.length, false);
|
|
1942
|
+
// A file-primary index has no JS path to fall through to, so a target the plane cannot
|
|
1943
|
+
// accept has to fail as the client error it is. Otherwise it throws out of
|
|
1944
|
+
// Float32Array.from or the traversal, is read as plane corruption, and unlinks a healthy
|
|
1945
|
+
// file — one malformed query costing a full reconstruction.
|
|
1946
|
+
if (this.filePrimary && plane) this.assertPlaneVector(target, 'Search target');
|
|
1947
|
+
// a non-file-primary query whose dimensionality differs from the graph's takes the JS
|
|
1948
|
+
// path, which tolerates the mismatch, rather than disabling the healthy plane
|
|
1949
|
+
if (plane && plane.dims === target.length && this.planeSearchReady(plane)) {
|
|
1950
|
+
const coverage = certified ?? (this.filePrimary ? this.queryCoverage(maxIndexLagMilliseconds) : undefined);
|
|
1951
|
+
try {
|
|
1952
|
+
const searched = this.searchPlane(plane, target, rerankK, effectiveEf, filter, filterState)
|
|
1953
|
+
.catch((error) => {
|
|
1954
|
+
if (error?.[NOT_A_PLANE_FAILURE]) throw error;
|
|
1955
|
+
// There is no JS graph behind a file-primary index: it stays unavailable until
|
|
1956
|
+
// its audit-backed rebuild succeeds.
|
|
1957
|
+
this.disablePlane(error);
|
|
1958
|
+
throw new ServerError('The native HNSW index is rebuilding', 503);
|
|
1959
|
+
})
|
|
1960
|
+
.then((entries) => {
|
|
1961
|
+
if (coverage) Object.defineProperty(entries, 'indexCoverage', { value: coverage });
|
|
1962
|
+
return entries;
|
|
1963
|
+
});
|
|
1964
|
+
if (coverage) Object.defineProperty(searched, 'indexCoverage', { value: coverage });
|
|
1965
|
+
return searched;
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
// Handle a throw raised before the asynchronous native search returns its promise.
|
|
1968
|
+
this.disablePlane(error);
|
|
1969
|
+
throw new ServerError('The native HNSW index is rebuilding', 503);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (this.filePrimary) {
|
|
1974
|
+
const state = this.derivedReadiness();
|
|
1975
|
+
if (state === 'unavailable') throw new ServerError('The native HNSW index is unavailable', 503);
|
|
1976
|
+
const planePath = this.planeFilePath();
|
|
1977
|
+
if (state !== 'ready' || (planePath && existsSync(planePath))) {
|
|
1978
|
+
throw new ServerError('The native HNSW index is rebuilding', 503);
|
|
1979
|
+
}
|
|
1980
|
+
// The absence of a file is not proof of an empty index — it is also the state just after
|
|
1981
|
+
// any process removes an unopenable one. Only the surviving node mappings prove it.
|
|
1982
|
+
if (this.hasNodeMappings()) {
|
|
1983
|
+
// A table taking no further writes never drains, so a query is the only thing left
|
|
1984
|
+
// that can notice the graph is gone and ask for it back.
|
|
1985
|
+
logger.error?.(`${this.indexStore.name} lost its native file while its node mappings survive`);
|
|
1986
|
+
this.derivedHost?.requestRebuild();
|
|
1987
|
+
throw new ServerError('The native HNSW index is rebuilding', 503);
|
|
1988
|
+
}
|
|
1989
|
+
const entries = withStats([], filterState);
|
|
1990
|
+
Object.defineProperty(entries, 'indexCoverage', {
|
|
1991
|
+
value: certified ?? this.queryCoverage(maxIndexLagMilliseconds),
|
|
1992
|
+
});
|
|
1993
|
+
return entries;
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
if (waiting) {
|
|
1997
|
+
context.signal?.throwIfAborted();
|
|
1998
|
+
const host = this.derivedHost;
|
|
1999
|
+
const state = host?.readiness().state;
|
|
2000
|
+
if (state !== 'ready') {
|
|
2001
|
+
throw new ServerError(
|
|
2002
|
+
`The native HNSW index is ${state === 'unavailable' ? 'unavailable' : 'rebuilding'}`,
|
|
2003
|
+
503
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
const searched = Promise.resolve(context.indexSearchStart).then(async () => {
|
|
2007
|
+
context.signal?.throwIfAborted();
|
|
2008
|
+
if (minResults === 0) return [];
|
|
2009
|
+
const started = derivedIndexTime(this.indexStore.rootStore);
|
|
2010
|
+
if (host.coverage(0)?.state !== 'current')
|
|
2011
|
+
await host.waitForCoverage(started, waitForIndexMilliseconds, context.signal);
|
|
2012
|
+
context.signal?.throwIfAborted();
|
|
2013
|
+
return searchNative({
|
|
2014
|
+
state: 'current',
|
|
2015
|
+
maxLagMilliseconds: maxIndexLagMilliseconds,
|
|
2016
|
+
lagUpperBoundMilliseconds: 0,
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
searched.catch(() => {});
|
|
2020
|
+
return searched;
|
|
2021
|
+
}
|
|
2022
|
+
const native = searchNative();
|
|
2023
|
+
if (native) return native;
|
|
2024
|
+
let entryPoint = this.getEntryPoint(txnOptions);
|
|
1163
2025
|
if (!entryPoint) return withStats([], filterState);
|
|
1164
2026
|
let entryPointId = entryPoint.id;
|
|
1165
2027
|
let results: Candidate[] = [];
|
|
@@ -1177,7 +2039,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1177
2039
|
entryPoint,
|
|
1178
2040
|
l === 0 ? effectiveEf : ROUTING_EF,
|
|
1179
2041
|
l,
|
|
1180
|
-
|
|
2042
|
+
txnOptions,
|
|
1181
2043
|
distanceFunction,
|
|
1182
2044
|
l === 0 ? filter : undefined,
|
|
1183
2045
|
l === 0 ? filterState : undefined
|
|
@@ -1193,6 +2055,7 @@ export class HierarchicalNavigableSmallWorld {
|
|
|
1193
2055
|
results = results.filter((candidate) =>
|
|
1194
2056
|
limitInclusive ? candidate.distance <= limit : candidate.distance < limit
|
|
1195
2057
|
);
|
|
2058
|
+
if (results.length > rerankK) results = results.slice(0, rerankK);
|
|
1196
2059
|
return withStats(
|
|
1197
2060
|
results.map((candidate) => ({
|
|
1198
2061
|
// we return the result as an entry so we can provide distance as metadata
|