okengine 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +20 -10
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +63 -36
- package/site/content/docs/meta.json +8 -7
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +257 -43
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -144
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -114
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- package/src/drivers/ollama.ts +0 -14
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Search"
|
|
3
|
+
description: "Built-in hybrid search on SQL tables — BM25 full-text, LSH vectors, RRF fusion — plus optional external index engines."
|
|
4
|
+
icon: "Search"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Built-in hybrid search ranks ordinary SQL rows. Mark text columns with `.searchable()` for BM25, chain `.embed()` when you also want semantic LSH, then call `fx.store(db).search` from a Flow.
|
|
9
|
+
|
|
10
|
+
It runs on PostgreSQL 15+ (`postgres` / `pglite`) with GIN + B-tree — **no extra extensions**. `store.index` (Meilisearch / pgvector) stays for engines you host separately.
|
|
11
|
+
|
|
12
|
+
For developers ranking rows on okengine — mark columns, query with `fx.store(db).search`, read `data` + `meta`.
|
|
13
|
+
|
|
14
|
+
<Callout title="The one rule">
|
|
15
|
+
`.searchable()` is free BM25 math on the table. `.embed()` is a separate chain that starts an
|
|
16
|
+
async, costed AI pipeline — never a boolean next to `weight`. Bare `.searchable()` needs no `ai`
|
|
17
|
+
element.
|
|
18
|
+
</Callout>
|
|
19
|
+
|
|
20
|
+
## Smallest Example
|
|
21
|
+
|
|
22
|
+
<Steps>
|
|
23
|
+
|
|
24
|
+
<Step>
|
|
25
|
+
### Mark columns and bind a route
|
|
26
|
+
|
|
27
|
+
```typescript title="src/db/schema.decl.ts"
|
|
28
|
+
import { field, store } from "okengine";
|
|
29
|
+
|
|
30
|
+
export const articles = store.schema.table("articles", {
|
|
31
|
+
id: field.text().primaryKey(),
|
|
32
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
33
|
+
body: field.text().searchable(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const db = store.sql("app", { schema: { articles } });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```typescript title="src/flows/articles/search.ts"
|
|
40
|
+
import { on, flow, http } from "okengine";
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
import { articles, db } from "@/schema";
|
|
43
|
+
|
|
44
|
+
export const search = on(
|
|
45
|
+
http.get(),
|
|
46
|
+
flow({
|
|
47
|
+
in: z.object({ q: z.string() }),
|
|
48
|
+
do: async ({ q }, fx) => {
|
|
49
|
+
const result = await fx.store(db).search(articles, {
|
|
50
|
+
query: q,
|
|
51
|
+
limit: 20,
|
|
52
|
+
});
|
|
53
|
+
return fx.json.ok(result.data, { meta: result.meta });
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
</Step>
|
|
60
|
+
|
|
61
|
+
<Step>
|
|
62
|
+
### Push schema and call
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
oke db push
|
|
66
|
+
curl -X GET "http://localhost:6530/articles/search?q=refund" \
|
|
67
|
+
-H "accept: application/json"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Response:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"data": [{ "id": "a1", "title": "Refund policy", "body": "…" }],
|
|
75
|
+
"error": null,
|
|
76
|
+
"meta": { "engine": ["bm25"], "limit": 20 }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
</Step>
|
|
81
|
+
|
|
82
|
+
</Steps>
|
|
83
|
+
|
|
84
|
+
<Callout title="BM25 needs no AI">
|
|
85
|
+
The smallest loop is full-text only. Chain `.embed()` when you want semantic neighbors — see
|
|
86
|
+
[Progressive Patterns](#progressive-patterns). `.embed()` without a resolvable `model` + `dims`
|
|
87
|
+
fails loud (`SearchConfigError`).
|
|
88
|
+
</Callout>
|
|
89
|
+
|
|
90
|
+
## Progressive Patterns
|
|
91
|
+
|
|
92
|
+
From BM25-only ranking to hybrid LSH, fusion, and opt-in rerank:
|
|
93
|
+
|
|
94
|
+
<Tabs items={["BM25", "Hybrid", "Fusion", "Rerank"]}>
|
|
95
|
+
|
|
96
|
+
<Tab value="BM25">
|
|
97
|
+
|
|
98
|
+
Title carries twice the BM25F field weight of body. No `ai` element, no `.embed()`:
|
|
99
|
+
|
|
100
|
+
```typescript title="src/db/schema.decl.ts"
|
|
101
|
+
import { field, store } from "okengine";
|
|
102
|
+
|
|
103
|
+
export const articles = store.schema.table("articles", {
|
|
104
|
+
id: field.text().primaryKey(),
|
|
105
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
106
|
+
body: field.text().searchable(),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`weight` must be a finite number **> 0** (default `1`). Only `text` / `varchar` / `char`.
|
|
111
|
+
|
|
112
|
+
</Tab>
|
|
113
|
+
|
|
114
|
+
<Tab value="Hybrid">
|
|
115
|
+
|
|
116
|
+
Set the project default once. Bare `.embed()` inherits; per-field `{ model?, dims? }` overrides:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
oke({
|
|
120
|
+
store: {
|
|
121
|
+
search: {
|
|
122
|
+
embed: { model: embedder, dims: 768 },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const articles = store.schema.table("articles", {
|
|
128
|
+
id: field.text().primaryKey(),
|
|
129
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
130
|
+
body: field.text().searchable().embed(),
|
|
131
|
+
caption: field.text().searchable().embed({ model: captionEmbedder }),
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Consequence:** schema columns with `.embed()` stamp `lsh` on `meta.engine`. Fusion
|
|
136
|
+
runs only when query + stored vectors produce LSH hits (`meta.fusedBy`).
|
|
137
|
+
|
|
138
|
+
</Tab>
|
|
139
|
+
|
|
140
|
+
<Tab value="Fusion">
|
|
141
|
+
|
|
142
|
+
Default fusion is Reciprocal Rank Fusion with **k = 60**. Weighted fusion is opt-in:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const result = await fx.store(db).search(articles, {
|
|
146
|
+
query: q,
|
|
147
|
+
fuse: { strategy: "rrf", k: 60 },
|
|
148
|
+
// fuse: { strategy: "weighted", weights: { bm25: 0.4, vector: 0.6 } },
|
|
149
|
+
limit: 20,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
RRF is the robust default. Weighted scores are min-max normalized per list first.
|
|
154
|
+
|
|
155
|
+
</Tab>
|
|
156
|
+
|
|
157
|
+
<Tab value="Rerank">
|
|
158
|
+
|
|
159
|
+
Rerank is **off** until you pass a prompt. It never silently calls `fx.ask`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const result = await fx.store(db).search(articles, {
|
|
163
|
+
query: q,
|
|
164
|
+
rerank: { model: "search.rerank" },
|
|
165
|
+
limit: 20,
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The prompt receives `{ query, docs }` and should return `{ rankedIds }`. Missing or empty
|
|
170
|
+
`rankedIds` leaves the fused order unchanged.
|
|
171
|
+
|
|
172
|
+
</Tab>
|
|
173
|
+
|
|
174
|
+
</Tabs>
|
|
175
|
+
|
|
176
|
+
## Field Reference
|
|
177
|
+
|
|
178
|
+
| Chain | Signature | Default | AI? | Meaning |
|
|
179
|
+
| --------------- | --------------------------- | ----------------------- | --- | -------------------------------------------------------------------------- |
|
|
180
|
+
| `.searchable()` | `.searchable({ weight? })` | `weight: 1` | No | BM25F field weight (applied to term frequency **before** saturation / IDF) |
|
|
181
|
+
| `.embed()` | `.embed({ model?, dims? })` | inherit project default | Yes | Async embedding + LSH on that searchable column |
|
|
182
|
+
|
|
183
|
+
`.embed()` without a prior `.searchable()` throws:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
.embed() requires a prior .searchable() on the same field — weight is free SQL math; embed is an async AI pipeline
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| `oke({ store: { search: { embed } } })` | Type | Meaning |
|
|
190
|
+
| --------------------------------------- | -------------------------------- | ---------------------------------- |
|
|
191
|
+
| `embed.model` | `ai.model` handle or name string | Required when the block is present |
|
|
192
|
+
| `embed.dims` | positive integer | Required when the block is present |
|
|
193
|
+
|
|
194
|
+
Per-field values win when set. Bare `.embed()` with neither a field option nor a project
|
|
195
|
+
default throws `SearchConfigError`:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
SearchConfigError: articles.body: .embed() needs model and dims — set oke({ store: { search: { embed: { model, dims } } } }) or pass them on .embed({ model, dims })
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Query Options
|
|
202
|
+
|
|
203
|
+
`fx.store(db).search(table, options)` — `query` / `fuse` / `rerank` are search-specific.
|
|
204
|
+
The rest is the same list grammar used by `store.resource` lists and `liveQuery`.
|
|
205
|
+
|
|
206
|
+
| Option | Type | Default | Meaning |
|
|
207
|
+
| ------------- | ------------------------------ | --------------------------------- | ---------------------------------------------------------------------- |
|
|
208
|
+
| `query` | `string` | _(required)_ | Relevance string (BM25 ± LSH). **Not** list-grammar `?search=` |
|
|
209
|
+
| `fuse` | `{ strategy?, k?, weights? }` | RRF, `k: 60` | Rank fusion when LSH hits exist |
|
|
210
|
+
| `rerank` | `false` \| `{ model }` | `false` | Opt-in `fx.ask` after fusion |
|
|
211
|
+
| `limit` | `number` | `20` | Page size (capped by `maxLimit`) |
|
|
212
|
+
| `maxLimit` | `number` | `100` | Cap on `limit` / `?limit=` |
|
|
213
|
+
| `filter` | `"all"` \| columns \| `"none"` | `"none"` | Whitelist for `filterInput` column filters |
|
|
214
|
+
| `filterInput` | object | `{}` | PostgREST-shaped filters (`status: "eq.active"`, `limit`, `cursor`, …) |
|
|
215
|
+
| `mode` | `"cursor"` \| `"offset"` | `"offset"` unless `cursor` is set | Pagination |
|
|
216
|
+
| `cursor` | columns | `[]` | Keyset columns |
|
|
217
|
+
| `order` | column scope | cursor columns, else `"all"` | `?order=` |
|
|
218
|
+
| `search` | column scope | `"none"` | List-grammar `?search=` / `?q=` LIKE — unused by hybrid `query` |
|
|
219
|
+
|
|
220
|
+
Result:
|
|
221
|
+
|
|
222
|
+
| Field | Meaning |
|
|
223
|
+
| -------------- | ----------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `data` | Ranked rows (PK + searchable text + stored embeddings when present) |
|
|
225
|
+
| `meta.engine` | `["bm25"]` or `["bm25", "lsh"]` — from schema (`.embed()` columns), not from whether fusion ran |
|
|
226
|
+
| `meta.fusedBy` | `"rrf"` or `"weighted"` — omitted when there are no vector hits |
|
|
227
|
+
| `meta.rrfK` | RRF damping constant — omitted unless RRF ran |
|
|
228
|
+
| `meta.limit` | Effective page size |
|
|
229
|
+
|
|
230
|
+
```typescript title="src/flows/articles/search.ts"
|
|
231
|
+
const { data, meta } = await fx.store(db).search(articles, {
|
|
232
|
+
query: "refund policy",
|
|
233
|
+
filter: [articles.status],
|
|
234
|
+
filterInput: { status: "eq.active" },
|
|
235
|
+
limit: 20,
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Consequence:** `filter: "none"` (the default) rejects unknown column keys in
|
|
240
|
+
`filterInput` — `unknown list param "status"`. Pass `filter: [articles.status]` or
|
|
241
|
+
`filter: "all"` before sending column filters.
|
|
242
|
+
|
|
243
|
+
## Two Surfaces
|
|
244
|
+
|
|
245
|
+
These share English words and are **not** the same API. Mixing them up ranks the
|
|
246
|
+
wrong way (or does not rank at all).
|
|
247
|
+
|
|
248
|
+
| Surface | Call | Parameter | Physics |
|
|
249
|
+
| -------------------- | -------------------------------------------------------- | ------------------- | ---------------------------------------------------------------- |
|
|
250
|
+
| List / live grammar | `store.resource` lists, `liveQuery` | `?search=` or `?q=` | Substring `LIKE %term%` on a column whitelist (default `"none"`) |
|
|
251
|
+
| Hybrid SQL search | `fx.store(db).search(table, { query })` | `query` | BM25 (± LSH) relevance ranking |
|
|
252
|
+
| Index / embed helper | `fx.search(embed, query)` / `fx.store(indexDecl).search` | vector or text | External `store.index` engine — not this table |
|
|
253
|
+
|
|
254
|
+
Side by side:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// 1) List grammar — substring filter (NOT BM25)
|
|
258
|
+
// GET /articles?search=refund&status=eq.active
|
|
259
|
+
await liveQuery(fx, articles, input, {
|
|
260
|
+
search: [articles.title],
|
|
261
|
+
filter: [articles.status],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 2) Hybrid search — BM25 / LSH relevance (NOT LIKE)
|
|
265
|
+
await fx.store(db).search(articles, {
|
|
266
|
+
query: "refund policy",
|
|
267
|
+
filter: [articles.status],
|
|
268
|
+
filterInput: { status: "eq.active", limit: "20" },
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`search()` reuses the list grammar for ordinary column filters, limit, and cursor
|
|
273
|
+
pagination. Only `query`, `fuse`, and `rerank` are search-specific.
|
|
274
|
+
|
|
275
|
+
## Declaring Columns
|
|
276
|
+
|
|
277
|
+
Each searchable column binds with `field.text().searchable(…)` (optionally `.embed()`):
|
|
278
|
+
|
|
279
|
+
<Tabs items={["Searchable", "Embed", "Project default", "Weights"]}>
|
|
280
|
+
|
|
281
|
+
<Tab value="Searchable">
|
|
282
|
+
|
|
283
|
+
Mark the text fields you want ranked. No AI, no shadow vector columns:
|
|
284
|
+
|
|
285
|
+
```typescript title="src/db/schema.decl.ts"
|
|
286
|
+
import { field, store } from "okengine";
|
|
287
|
+
|
|
288
|
+
export const articles = store.schema.table("articles", {
|
|
289
|
+
id: field.id().primaryKey(),
|
|
290
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
291
|
+
body: field.text().searchable().notNull(),
|
|
292
|
+
status: field.text().notNull(),
|
|
293
|
+
createdAt: field.timestamp().notNull().now(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
export const db = store.sql("app", { schema: { articles } });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
After `oke db push`, the table gets a generated `tsvector` + GIN index for candidate
|
|
300
|
+
retrieval (`plainto_tsquery('english', …)`).
|
|
301
|
+
|
|
302
|
+
</Tab>
|
|
303
|
+
|
|
304
|
+
<Tab value="Embed">
|
|
305
|
+
|
|
306
|
+
Chain `.embed()` **after** `.searchable()`. Pass `{ model, dims }` on the field, or
|
|
307
|
+
inherit the project default:
|
|
308
|
+
|
|
309
|
+
```typescript title="src/db/schema.decl.ts"
|
|
310
|
+
import { field, store, ai } from "okengine";
|
|
311
|
+
|
|
312
|
+
const embedder = ai.model("embedder", {
|
|
313
|
+
provider: "openai-compatible",
|
|
314
|
+
model: "nomic-embed-text",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const articles = store.schema.table("articles", {
|
|
318
|
+
id: field.id().primaryKey(),
|
|
319
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
320
|
+
body: field.text().searchable().embed({ model: embedder, dims: 768 }),
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Consequence:** writers never call `fx.embed` — a system CDC flow embeds after commit.
|
|
325
|
+
See [Embedding Pipeline](#embedding-pipeline).
|
|
326
|
+
|
|
327
|
+
</Tab>
|
|
328
|
+
|
|
329
|
+
<Tab value="Project default">
|
|
330
|
+
|
|
331
|
+
Stamp `oke({ store: { search: { embed } } })` once so bare `.embed()` inherits:
|
|
332
|
+
|
|
333
|
+
```typescript title="src/core.ts"
|
|
334
|
+
import { ai, oke } from "okengine";
|
|
335
|
+
|
|
336
|
+
const embedder = ai.model("embedder", {
|
|
337
|
+
provider: "openai-compatible",
|
|
338
|
+
model: "nomic-embed-text",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
oke({
|
|
342
|
+
store: {
|
|
343
|
+
search: {
|
|
344
|
+
embed: { model: embedder, dims: 768 },
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
```typescript title="src/db/schema.decl.ts"
|
|
351
|
+
body: field.text().searchable().embed(), // inherits model + dims
|
|
352
|
+
caption: field.text().searchable().embed({ model: captionEmbedder }), // dims still inherit
|
|
353
|
+
alt: field.text().searchable().embed({ model: captionEmbedder, dims: 384 }),
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Project block without `dims` or `model` fails extract:
|
|
357
|
+
|
|
358
|
+
```text
|
|
359
|
+
extract: oke({ store: { search: { embed } } }) requires dims: positive integer
|
|
360
|
+
extract: oke({ store: { search: { embed } } }) requires model (ai.model handle or name string)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
</Tab>
|
|
364
|
+
|
|
365
|
+
<Tab value="Weights">
|
|
366
|
+
|
|
367
|
+
`weight` multiplies term frequency **before** Robertson–Zaragoza saturation
|
|
368
|
+
(**k1 = 1.2**, **b = 0.75**). A `weight: 2` title is not “twice the final score”:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
372
|
+
body: field.text().searchable(), // weight: 1
|
|
373
|
+
tags: field.text().searchable({ weight: 0.5 }),
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Invalid weights throw at declare time:
|
|
377
|
+
|
|
378
|
+
```text
|
|
379
|
+
searchable({ weight }) must be a finite number > 0 (got …)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
</Tab>
|
|
383
|
+
|
|
384
|
+
</Tabs>
|
|
385
|
+
|
|
386
|
+
## Running Search
|
|
387
|
+
|
|
388
|
+
Bind a Flow, pass `query`, return `data` + `meta`:
|
|
389
|
+
|
|
390
|
+
<Tabs items={["BM25 route", "Filtered", "Hybrid response", "Cursor"]}>
|
|
391
|
+
|
|
392
|
+
<Tab value="BM25 route">
|
|
393
|
+
|
|
394
|
+
Full HTTP loop with envelope `meta` from the search result:
|
|
395
|
+
|
|
396
|
+
```typescript title="src/flows/articles/search.ts"
|
|
397
|
+
import { on, flow, http } from "okengine";
|
|
398
|
+
import { z } from "zod";
|
|
399
|
+
import { articles, db } from "@/schema";
|
|
400
|
+
|
|
401
|
+
export const search = on(
|
|
402
|
+
http.get(),
|
|
403
|
+
flow({
|
|
404
|
+
in: z.object({ q: z.string().min(1) }),
|
|
405
|
+
do: async ({ q }, fx) => {
|
|
406
|
+
const result = await fx.store(db).search(articles, {
|
|
407
|
+
query: q,
|
|
408
|
+
limit: 20,
|
|
409
|
+
});
|
|
410
|
+
return fx.json.ok(result.data, { meta: result.meta });
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
curl -X GET "http://localhost:6530/articles/search?q=refund+policy" \
|
|
418
|
+
-H "accept: application/json"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
</Tab>
|
|
422
|
+
|
|
423
|
+
<Tab value="Filtered">
|
|
424
|
+
|
|
425
|
+
Whitelist columns, then pass PostgREST-shaped filters in `filterInput`:
|
|
426
|
+
|
|
427
|
+
```typescript title="src/flows/articles/search.ts"
|
|
428
|
+
import { on, flow, http } from "okengine";
|
|
429
|
+
import { z } from "zod";
|
|
430
|
+
import { articles, db } from "@/schema";
|
|
431
|
+
|
|
432
|
+
export const search = on(
|
|
433
|
+
http.get(),
|
|
434
|
+
flow({
|
|
435
|
+
in: z.object({
|
|
436
|
+
q: z.string().min(1),
|
|
437
|
+
status: z.string().optional(),
|
|
438
|
+
}),
|
|
439
|
+
do: async ({ q, status }, fx) => {
|
|
440
|
+
const result = await fx.store(db).search(articles, {
|
|
441
|
+
query: q,
|
|
442
|
+
filter: [articles.status],
|
|
443
|
+
filterInput: {
|
|
444
|
+
...(status ? { status: `eq.${status}` } : {}),
|
|
445
|
+
limit: "20",
|
|
446
|
+
},
|
|
447
|
+
limit: 20,
|
|
448
|
+
});
|
|
449
|
+
return fx.json.ok(result.data, { meta: result.meta });
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Filter ops match resource lists: `eq` · `neq` · `gt` · `gte` · `lt` · `lte` ·
|
|
456
|
+
`like` · `ilike` · `in` · `is` (+ `not.` prefix).
|
|
457
|
+
|
|
458
|
+
</Tab>
|
|
459
|
+
|
|
460
|
+
<Tab value="Hybrid response">
|
|
461
|
+
|
|
462
|
+
When `.embed()` columns exist and vectors score, `meta` gains fusion fields:
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"data": [{ "id": "a1", "title": "Refund policy", "body": "…" }],
|
|
467
|
+
"error": null,
|
|
468
|
+
"meta": {
|
|
469
|
+
"engine": ["bm25", "lsh"],
|
|
470
|
+
"fusedBy": "rrf",
|
|
471
|
+
"rrfK": 60,
|
|
472
|
+
"limit": 20
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
If query embedding is not wired at boot, ranking stays lexical — `fusedBy` is
|
|
478
|
+
omitted even when `meta.engine` lists `"lsh"` from the schema.
|
|
479
|
+
|
|
480
|
+
</Tab>
|
|
481
|
+
|
|
482
|
+
<Tab value="Cursor">
|
|
483
|
+
|
|
484
|
+
Keyset pagination reuses list-grammar `cursor` / `filterInput`:
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const result = await fx.store(db).search(articles, {
|
|
488
|
+
query: q,
|
|
489
|
+
mode: "cursor",
|
|
490
|
+
cursor: [articles.createdAt, articles.id],
|
|
491
|
+
filterInput: { cursor: lastCursor, limit: "20" },
|
|
492
|
+
limit: 20,
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Consequence:** keyset pages stay stable under inserts the same way resource
|
|
497
|
+
lists do — prefer cursor when the corpus grows under concurrent writes.
|
|
498
|
+
|
|
499
|
+
</Tab>
|
|
500
|
+
|
|
501
|
+
</Tabs>
|
|
502
|
+
|
|
503
|
+
## Embedding Pipeline
|
|
504
|
+
|
|
505
|
+
<Callout title="Detailed section">
|
|
506
|
+
If you only need BM25, skip this. Writer Flows **never** call `fx.embed` — a system-owned durable
|
|
507
|
+
CDC flow embeds after commit. A just-written row may be missing from semantic results for a short
|
|
508
|
+
interval.
|
|
509
|
+
</Callout>
|
|
510
|
+
|
|
511
|
+
`.embed()` starts an async pipeline: after the row commits, the runtime embeds
|
|
512
|
+
changed text, packs an LSH bucket (64 hyperplanes), and stores both beside the row.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
body: field.text().searchable().embed(), // inherits oke({ store: { search: { embed } } })
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
<Accordions>
|
|
519
|
+
|
|
520
|
+
<Accordion title="What your Flow does not do">
|
|
521
|
+
App writers insert and update as usual. They do **not** gain `effects.embeds`.
|
|
522
|
+
The operator-plane flow `_oke_search_embed_<table>` owns `fx.embed` + journaled
|
|
523
|
+
`fx.step`. Deletes drop the row (and its shadow columns) — nothing extra to run.
|
|
524
|
+
</Accordion>
|
|
525
|
+
|
|
526
|
+
<Accordion title="Eventual consistency">
|
|
527
|
+
BM25 candidates update with the generated `tsvector` on write. LSH neighbors
|
|
528
|
+
wait on the embed step.
|
|
529
|
+
|
|
530
|
+
**Consequence:** lexical hits can appear before semantic ones. That window is
|
|
531
|
+
intentional. Do not poll `fx.embed` from the writer to “close” it.
|
|
532
|
+
|
|
533
|
+
</Accordion>
|
|
534
|
+
|
|
535
|
+
<Accordion title="What push creates">
|
|
536
|
+
`oke db push` adds search DDL when columns are `.searchable()` / `.embed()`:
|
|
537
|
+
|
|
538
|
+
| Object | Role |
|
|
539
|
+
| ------------------------------------- | ---------------------------------------------------------- |
|
|
540
|
+
| Generated `tsvector` + GIN | BM25 candidate retrieval (`plainto_tsquery('english', …)`) |
|
|
541
|
+
| `real[]` embedding column | Stored vector per `.embed()` field |
|
|
542
|
+
| `bigint` LSH column + B-tree | Bucket lookup (Hamming-1 neighbors included) |
|
|
543
|
+
| Corpus stats / DF / hyperplane tables | IDF, average length, stable LSH planes |
|
|
544
|
+
|
|
545
|
+
Hyperplanes insert once (`ON CONFLICT DO NOTHING`) and are **never** regenerated.
|
|
546
|
+
Changing `dims` on a live column leaves the old planes in place — you will hit a
|
|
547
|
+
length `SearchConfigError` until those rows are rebuilt.
|
|
548
|
+
|
|
549
|
+
</Accordion>
|
|
550
|
+
|
|
551
|
+
<Accordion title="Missing AI / missing dims">
|
|
552
|
+
`.embed()` without a configured `ai` element:
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
SearchConfigError: articles.body: .embed() requires a configured ai element (ai.model / ai.embed). Remove .embed() for BM25-only search, or declare an embedding model.
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Project default block without `dims` or `model`:
|
|
559
|
+
|
|
560
|
+
```text
|
|
561
|
+
extract: oke({ store: { search: { embed } } }) requires dims: positive integer
|
|
562
|
+
extract: oke({ store: { search: { embed } } }) requires model (ai.model handle or name string)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
</Accordion>
|
|
566
|
+
|
|
567
|
+
</Accordions>
|
|
568
|
+
|
|
569
|
+
## Fusion
|
|
570
|
+
|
|
571
|
+
<Callout title="Detailed section">
|
|
572
|
+
If you only need BM25, skip this. Fusion runs only when both BM25 and LSH hit lists exist — then
|
|
573
|
+
ranks are fused and truncated to `limit`.
|
|
574
|
+
</Callout>
|
|
575
|
+
|
|
576
|
+
Candidates are oversampled (`max(limit × 5, 50)`, capped at 500) before fusion.
|
|
577
|
+
|
|
578
|
+
| `fuse.strategy` | Formula | Default knobs |
|
|
579
|
+
| ----------------- | --------------------------------- | ---------------------------------------------------------------------------- |
|
|
580
|
+
| `"rrf"` (default) | Σ `1 / (k + rank)` | `k: 60` (Cormack, Clarke, Büttcher — SIGIR 2009; MAP flat for k ∈ [20, 100]) |
|
|
581
|
+
| `"weighted"` | min-max per list, then linear mix | `weights.bm25` / `weights.vector` default `0.5` each |
|
|
582
|
+
|
|
583
|
+
<Accordions>
|
|
584
|
+
|
|
585
|
+
<Accordion title="RRF (default)">
|
|
586
|
+
Reciprocal Rank Fusion ignores raw score scales — only ranks matter:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
await fx.store(db).search(articles, {
|
|
590
|
+
query: q,
|
|
591
|
+
fuse: { strategy: "rrf", k: 60 },
|
|
592
|
+
limit: 20,
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
`meta.fusedBy` is `"rrf"` and `meta.rrfK` echoes the damping constant when RRF ran.
|
|
597
|
+
|
|
598
|
+
</Accordion>
|
|
599
|
+
|
|
600
|
+
<Accordion title="Weighted">
|
|
601
|
+
Opt-in linear mix after per-list min-max normalization:
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
await fx.store(db).search(articles, {
|
|
605
|
+
query: q,
|
|
606
|
+
fuse: {
|
|
607
|
+
strategy: "weighted",
|
|
608
|
+
weights: { bm25: 0.4, vector: 0.6 },
|
|
609
|
+
},
|
|
610
|
+
limit: 20,
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
`meta.fusedBy` is `"weighted"`; `rrfK` is omitted.
|
|
615
|
+
|
|
616
|
+
</Accordion>
|
|
617
|
+
|
|
618
|
+
<Accordion title="BM25F constants">
|
|
619
|
+
BM25F uses Robertson–Zaragoza saturation: **k1 = 1.2**, **b = 0.75**. Field
|
|
620
|
+
`weight` multiplies term frequency *before* that saturation.
|
|
621
|
+
|
|
622
|
+
LSH uses **64** hyperplanes (fits a `bigint` bit pack). Query-time lookup includes
|
|
623
|
+
the exact bucket and Hamming-1 neighbors.
|
|
624
|
+
|
|
625
|
+
</Accordion>
|
|
626
|
+
|
|
627
|
+
<Accordion title="No vector hits">
|
|
628
|
+
When LSH produces no scored neighbors (or query embedding is unwired), order is pure BM25.
|
|
629
|
+
`fusedBy` / `rrfK` are omitted. `meta.engine` may still list `"lsh"` if the table declared
|
|
630
|
+
`.embed()` columns.
|
|
631
|
+
</Accordion>
|
|
632
|
+
|
|
633
|
+
</Accordions>
|
|
634
|
+
|
|
635
|
+
## Rerank
|
|
636
|
+
|
|
637
|
+
Rerank is a second, optional pass after fusion. Declare a prompt, then pass its
|
|
638
|
+
name — never enabled by default:
|
|
639
|
+
|
|
640
|
+
```typescript title="src/ai/search-rerank.ts"
|
|
641
|
+
import { ai } from "okengine";
|
|
642
|
+
import { z } from "zod";
|
|
643
|
+
|
|
644
|
+
const reranker = ai.model("reranker", {
|
|
645
|
+
provider: "openai-compatible",
|
|
646
|
+
model: "llama3.1",
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
export const searchRerank = reranker.prompt("search.rerank", {
|
|
650
|
+
in: z.object({
|
|
651
|
+
query: z.string(),
|
|
652
|
+
docs: z.array(z.object({ id: z.string(), text: z.string(), score: z.number() })),
|
|
653
|
+
}),
|
|
654
|
+
out: z.object({ rankedIds: z.array(z.string()) }),
|
|
655
|
+
budget: { maxCostPerCall: 0.02 },
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
```typescript title="src/flows/articles/search.ts"
|
|
660
|
+
const result = await fx.store(db).search(articles, {
|
|
661
|
+
query: q,
|
|
662
|
+
rerank: { model: "search.rerank" },
|
|
663
|
+
limit: 20,
|
|
664
|
+
});
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
The runtime calls `fx.ask` with `{ query, docs }` where each doc’s `text` is the
|
|
668
|
+
concatenated searchable fields. Return `{ rankedIds }` in preferred order.
|
|
669
|
+
Missing or empty `rankedIds` keeps the fused order.
|
|
670
|
+
|
|
671
|
+
**Consequence:** budgets on the prompt (`maxCostPerCall`) are the cost guardrail —
|
|
672
|
+
search itself does not invent a second limit.
|
|
673
|
+
|
|
674
|
+
## Backfill
|
|
675
|
+
|
|
676
|
+
`oke db push` applies shadow columns and indexes. It **never** silently backfills
|
|
677
|
+
a large table. Run the rebuild yourself:
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
oke db search-backfill <table> [--batch=32]
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
| Flag | Default | Meaning |
|
|
684
|
+
| --------- | ------------ | -------------------------------------------------------------------------- |
|
|
685
|
+
| `<table>` | _(required)_ | SQL table name in the Manifest |
|
|
686
|
+
| `--batch` | `32` | Rows per page (embed batches pause between pages for provider rate limits) |
|
|
687
|
+
| `--env` | config env | `dev` \| `test` \| `prod` |
|
|
688
|
+
|
|
689
|
+
Doctor warns when searchable columns land on existing rows:
|
|
690
|
+
|
|
691
|
+
```text
|
|
692
|
+
table "articles" has searchable/embed columns on existing data — run `oke db search-backfill articles` (never auto on push)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
<Accordions>
|
|
696
|
+
|
|
697
|
+
<Accordion title="Low corpus warning">
|
|
698
|
+
Corpus stats below **100** rows print:
|
|
699
|
+
|
|
700
|
+
```text
|
|
701
|
+
[oke db search-backfill] warn: table "articles" has only 12 rows — IDF/BM25 corpus statistics are not meaningful yet (threshold 100)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Ranking still runs; IDF is unstable until the corpus grows past 100 rows.
|
|
705
|
+
|
|
706
|
+
</Accordion>
|
|
707
|
+
|
|
708
|
+
<Accordion title="Unknown table">
|
|
709
|
+
Cause:
|
|
710
|
+
|
|
711
|
+
```text
|
|
712
|
+
search-backfill: table "articles" not found in Manifest
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Use the Manifest SQL table name (the string passed to `store.schema.table`), not
|
|
716
|
+
a Flow name.
|
|
717
|
+
|
|
718
|
+
</Accordion>
|
|
719
|
+
|
|
720
|
+
<Accordion title="search-backfill needs a live SQL URL">
|
|
721
|
+
Cause: `oke db search-backfill: no DATABASE_URL / OKE_STORE_SQL_URL / OKE_PGLITE_URL — cannot open SQL`.
|
|
722
|
+
Set a connection URL (compose `.env.local` or process env), then:
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
oke db search-backfill articles --batch 500
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
The CLI opens SQL, extracts the Manifest, and calls `runSearchBackfill`. Never auto-runs on push.
|
|
729
|
+
|
|
730
|
+
</Accordion>
|
|
731
|
+
|
|
732
|
+
</Accordions>
|
|
733
|
+
|
|
734
|
+
## External Indexes
|
|
735
|
+
|
|
736
|
+
<Callout title="Not this capability">
|
|
737
|
+
`store.index` is a separate facet with its own drivers. Use it when you need typo-tolerant HTTP
|
|
738
|
+
search or a hosted vector engine — not as a substitute for `.searchable()` on the primary table.
|
|
739
|
+
</Callout>
|
|
740
|
+
|
|
741
|
+
<StoreIndexModes />
|
|
742
|
+
|
|
743
|
+
Index stays `memory` until you set `drivers.store.index` explicitly — there is no
|
|
744
|
+
silent fallback to Meilisearch or pgvector.
|
|
745
|
+
|
|
746
|
+
<Tabs items={["Meilisearch", "pgvector"]}>
|
|
747
|
+
|
|
748
|
+
<Tab value="Meilisearch">
|
|
749
|
+
|
|
750
|
+
Omit `{ dims }` — dimensions select a vector driver. Search takes a **string**:
|
|
751
|
+
|
|
752
|
+
```typescript title="src/db/indexes.ts"
|
|
753
|
+
import { store } from "okengine";
|
|
754
|
+
|
|
755
|
+
export const articlesIndex = store.index("articles");
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
const idx = fx.store(articlesIndex);
|
|
760
|
+
if (idx.driverId === "meilisearch") {
|
|
761
|
+
const { hits } = await idx.search(q, { topK: 20 });
|
|
762
|
+
return hits;
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
See [Meilisearch](/docs/recipes/meilisearch) for `oke.config.ts` pins and keys.
|
|
767
|
+
|
|
768
|
+
</Tab>
|
|
769
|
+
|
|
770
|
+
<Tab value="pgvector">
|
|
771
|
+
|
|
772
|
+
Pass `{ dims }`. Search takes a **vector** (usually from `fx.embed`):
|
|
773
|
+
|
|
774
|
+
```typescript title="src/db/indexes.ts"
|
|
775
|
+
import { store } from "okengine";
|
|
776
|
+
|
|
777
|
+
export const articlesIndex = store.index("articles", { dims: 768 });
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
```typescript
|
|
781
|
+
const idx = fx.store(articlesIndex);
|
|
782
|
+
if (idx.driverId === "pgvector" || idx.driverId === "memory") {
|
|
783
|
+
const vector = await fx.embed(embedder, q);
|
|
784
|
+
return await idx.search(vector, 20);
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
`memory` is the same vector shape (cosine) for tests. Driver ids:
|
|
789
|
+
`memory` · `pgvector` · `meilisearch`.
|
|
790
|
+
|
|
791
|
+
</Tab>
|
|
792
|
+
|
|
793
|
+
</Tabs>
|
|
794
|
+
|
|
795
|
+
## Measured latency & recall (G17)
|
|
796
|
+
|
|
797
|
+
Headline numbers from the live-Postgres G17 gate (`OKE_TEST_POSTGRES=1`, Bun 1.4.2, Apple M4, Postgres 16). Trend-analysis only — not an SLA. Full tables and EXPLAIN live in the repo load-test report: `src/bench/REPORT.md` (G17).
|
|
798
|
+
|
|
799
|
+
### When to stay on BM25 vs add LSH vs use an external index
|
|
800
|
+
|
|
801
|
+
| Corpus size | BM25 (text) p50 | LSH/hybrid p50 | LSH precision@10 vs exact cosine | Guidance |
|
|
802
|
+
| ----------- | --------------- | -------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
803
|
+
| ≤10k | ~1–4 ms | ~1–6 ms | ≈0 on this gate | Built-in hybrid is fine for ranking UX; do not market LSH recall |
|
|
804
|
+
| ~100k | ~40 ms | ~50–60 ms | ≈0 | Expect tens of ms; re-`EXPLAIN` after `ANALYZE` |
|
|
805
|
+
| ~1M | ~0.6–1.2 s | ~0.6–1.2 s | ≈0 | Prefer external `store.index` (pgvector / Meilisearch) for semantic recall at this scale |
|
|
806
|
+
|
|
807
|
+
**Honest LSH note:** random-hyperplane LSH (K=64, Hamming-1, ≤50 candidates) did **not** match exact brute-force cosine top-10 on the G17 corpus (precision@10 mostly **0**). Same class of honesty as earlier LSH-vs-HNSW design notes — use LSH as a cheap candidate hint inside RRF, not as a recall guarantee. BM25-only needs no AI and remains the smallest path.
|
|
808
|
+
|
|
809
|
+
**Query plan:** at N=100k, `EXPLAIN (ANALYZE, BUFFERS)` showed a **Seq Scan** with a tsvector/LSH Filter — not BitmapOr — despite GIN + B-tree indexes. Capture your own plan on production data before assuming an index path.
|
|
810
|
+
|
|
811
|
+
**Backfill:** `oke db search-backfill` is interrupt-safe to re-run (G17 killed at 2k/50k embeds, resumed to completion in ~29 s on that table).
|
|
812
|
+
|
|
813
|
+
## Requirements
|
|
814
|
+
|
|
815
|
+
Built-in hybrid search is a SQL-facet capability — not a fourth store facet.
|
|
816
|
+
|
|
817
|
+
| Need | Requirement |
|
|
818
|
+
| ---------- | ------------------------------------------------------ |
|
|
819
|
+
| Driver | `postgres` or `pglite` (PostgreSQL 15+) |
|
|
820
|
+
| Extensions | **None** — GIN + B-tree only |
|
|
821
|
+
| BM25 | At least one `.searchable()` text column |
|
|
822
|
+
| LSH | `.embed()` + configured `ai` + `model` / `dims` |
|
|
823
|
+
| Backfill | Explicit `oke db search-backfill` (never auto on push) |
|
|
824
|
+
|
|
825
|
+
## Troubleshooting
|
|
826
|
+
|
|
827
|
+
<Accordions>
|
|
828
|
+
|
|
829
|
+
<Accordion title="SearchConfigError — .embed() needs model and dims">
|
|
830
|
+
Cause: `SearchConfigError: {table}.{column}: .embed() needs model and dims — set oke({ store: { search: { embed: { model, dims } } } }) or pass them on .embed({ model, dims })`.
|
|
831
|
+
Set the project default, or pass `{ model, dims }` on that field.
|
|
832
|
+
</Accordion>
|
|
833
|
+
|
|
834
|
+
<Accordion title="SearchConfigError — .embed() requires a configured ai element">
|
|
835
|
+
Cause: `.embed() requires a configured ai element (ai.model / ai.embed). Remove .embed() for
|
|
836
|
+
BM25-only search, or declare an embedding model.` Drop `.embed()` for BM25-only, or declare
|
|
837
|
+
`ai.model` / `ai.embed`.
|
|
838
|
+
</Accordion>
|
|
839
|
+
|
|
840
|
+
<Accordion title=".embed() requires a prior .searchable()">
|
|
841
|
+
Cause: `.embed() requires a prior .searchable() on the same field — weight is free SQL math; embed
|
|
842
|
+
is an async AI pipeline`. Chain `.searchable()` first: `field.text().searchable().embed()`.
|
|
843
|
+
</Accordion>
|
|
844
|
+
|
|
845
|
+
<Accordion title="searchable() is only valid on text / varchar / char">
|
|
846
|
+
Cause: `field.{type}().searchable() is only valid on text / varchar / char columns`. Hybrid search
|
|
847
|
+
is a text pipeline — do not mark integers or timestamps.
|
|
848
|
+
</Accordion>
|
|
849
|
+
|
|
850
|
+
<Accordion title="searchable({ weight }) must be a finite number > 0">
|
|
851
|
+
Cause: `searchable({ weight }) must be a finite number > 0 (got …)`.
|
|
852
|
+
Omit `weight` for `1`, or pass a positive finite number.
|
|
853
|
+
</Accordion>
|
|
854
|
+
|
|
855
|
+
<Accordion title="search(): table must be a store.schema.table()">
|
|
856
|
+
Cause: `search(): table must be a store.schema.table() declaration with .searchable() columns`.
|
|
857
|
+
Pass the schema table, not a string name. At least one column needs `.searchable()`.
|
|
858
|
+
</Accordion>
|
|
859
|
+
|
|
860
|
+
<Accordion title="SearchConfigError — no .searchable() columns">
|
|
861
|
+
Cause: `SearchConfigError: {table}.*: no .searchable() columns on this table`. Mark the text
|
|
862
|
+
fields you want ranked before calling `.search()`.
|
|
863
|
+
</Accordion>
|
|
864
|
+
|
|
865
|
+
<Accordion title="unknown list param / unfilterable column">
|
|
866
|
+
Default `filter: "none"` rejects extra keys (`unknown list param "status"`). Whitelist with
|
|
867
|
+
`filter: […]` or `filter: "all"`. `limit` / `cursor` / `order` are always parsed.
|
|
868
|
+
</Accordion>
|
|
869
|
+
|
|
870
|
+
<Accordion title="missing hyperplanes">
|
|
871
|
+
Cause: `missing hyperplanes — run oke db search-backfill or ensure push applied search DDL`. Push
|
|
872
|
+
(or backfill) must run after `.embed()` is declared so LSH planes exist.
|
|
873
|
+
</Accordion>
|
|
874
|
+
|
|
875
|
+
<Accordion title="embedding length !== declared dims">
|
|
876
|
+
Cause: `query embedding length {n} !== declared dims {d}` / `stored embedding length {n} !==
|
|
877
|
+
declared dims {d}`. Model output, field `dims`, and stored planes must match. Changing `dims` on a
|
|
878
|
+
live column does not regenerate planes.
|
|
879
|
+
</Accordion>
|
|
880
|
+
|
|
881
|
+
<Accordion title="Just-written row missing from semantic results">
|
|
882
|
+
Expected. Writer Flows do not embed. Wait for the CDC embed step, or rank with BM25 (`meta.engine`
|
|
883
|
+
includes `"bm25"` immediately after the `tsvector` write).
|
|
884
|
+
</Accordion>
|
|
885
|
+
|
|
886
|
+
<Accordion title="meta.engine lists lsh but fusedBy is missing">
|
|
887
|
+
Schema has `.embed()` columns, so `meta.engine` includes `"lsh"`. Fusion only runs when query +
|
|
888
|
+
stored vectors produce scored neighbors. Check that `embedQuery` is wired at boot and that
|
|
889
|
+
backfill / CDC wrote embeddings.
|
|
890
|
+
</Accordion>
|
|
891
|
+
|
|
892
|
+
<Accordion title="Doctor says run search-backfill">
|
|
893
|
+
Cause: `table "{name}" has searchable/embed columns on existing data — run oke db search-backfill{" "}
|
|
894
|
+
{name} (never auto on push)`. Push created shadow columns; corpus stats / embeddings still need an
|
|
895
|
+
explicit rebuild.
|
|
896
|
+
</Accordion>
|
|
897
|
+
|
|
898
|
+
<Accordion title="IDF/BM25 corpus statistics are not meaningful yet">
|
|
899
|
+
Cause: `[oke db search-backfill] warn: table "{name}" has only {n} rows — IDF/BM25 corpus
|
|
900
|
+
statistics are not meaningful yet (threshold 100)`. Ranking still runs; IDF is unstable until the
|
|
901
|
+
corpus grows past 100 rows.
|
|
902
|
+
</Accordion>
|
|
903
|
+
|
|
904
|
+
<Accordion title="CLI prints programmatic API / live SQL not wired">
|
|
905
|
+
Cause: `oke db search-backfill: use the programmatic runSearchBackfill(conn, manifest, {table})
|
|
906
|
+
API, or pass --table via CLI once a live SQL connection is wired for this project.` The subcommand
|
|
907
|
+
is registered (`--batch` default 32) and never auto-runs on push. Wire a live SQL connection, then
|
|
908
|
+
rerun.
|
|
909
|
+
</Accordion>
|
|
910
|
+
|
|
911
|
+
<Accordion title="I passed ?search= and got LIKE, not BM25">
|
|
912
|
+
Resource lists and `liveQuery` treat `?search=` / `?q=` as substring `LIKE`. Hybrid ranking is
|
|
913
|
+
`fx.store(db).search(table, {query})` — see [Two Surfaces](#two-surfaces).
|
|
914
|
+
</Accordion>
|
|
915
|
+
|
|
916
|
+
</Accordions>
|
|
917
|
+
|
|
918
|
+
## Learn more
|
|
919
|
+
|
|
920
|
+
- [Store](/docs/elements/store) — four facets; `fx.store` handles
|
|
921
|
+
- [SQL](/docs/elements/store/sql) — `store.schema.table`, `field.*`, list grammar
|
|
922
|
+
- [HTTP · Resources](/docs/elements/flow/http#resources) — list grammar (`?search=` LIKE, filters, cursor)
|
|
923
|
+
- [AI](/docs/elements/ai) — `ai.model`, `ai.prompt`, `fx.ask` / `fx.embed`
|
|
924
|
+
- [fx](/docs/reference/fx) — `fx.store(db).search` is a SQL read; `fx.search(embed, query)` is the index helper
|
|
925
|
+
- [Meilisearch](/docs/recipes/meilisearch) — `store.index` full-text driver
|
|
926
|
+
- [Configuration](/docs/reference/configuration) — `drivers.store.index` (`memory` · `pgvector` · `meilisearch`)
|
|
927
|
+
|
|
928
|
+
## Next
|
|
929
|
+
|
|
930
|
+
<Cards>
|
|
931
|
+
<Card
|
|
932
|
+
title="SQL"
|
|
933
|
+
description="Schema tables, field helpers, and store.resource CRUD."
|
|
934
|
+
href="/docs/elements/store/sql"
|
|
935
|
+
/>
|
|
936
|
+
<Card
|
|
937
|
+
title="AI"
|
|
938
|
+
description="Embedding models, prompts, and fx.embed."
|
|
939
|
+
href="/docs/elements/ai"
|
|
940
|
+
/>
|
|
941
|
+
<Card
|
|
942
|
+
title="Store Overview"
|
|
943
|
+
description="SQL · KV · files · index — one handle."
|
|
944
|
+
href="/docs/elements/store"
|
|
945
|
+
/>
|
|
946
|
+
</Cards>
|