okengine 0.18.4 → 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.
Files changed (512) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/index.mdx +25 -6
  5. package/site/content/docs/ai/llms-txt.mdx +1 -1
  6. package/site/content/docs/ai/meta.json +1 -1
  7. package/site/content/docs/ai/skills.mdx +4 -1
  8. package/site/content/docs/ai/try-it.mdx +58 -0
  9. package/site/content/docs/client/auth.mdx +208 -0
  10. package/site/content/docs/client/calling.mdx +451 -0
  11. package/site/content/docs/client/index.mdx +209 -0
  12. package/site/content/docs/client/live.mdx +234 -0
  13. package/site/content/docs/client/meta.json +5 -0
  14. package/site/content/docs/client/react.mdx +249 -0
  15. package/site/content/docs/elements/ai/agents.mdx +232 -0
  16. package/site/content/docs/elements/ai/index.mdx +341 -0
  17. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  18. package/site/content/docs/elements/ai/meta.json +5 -0
  19. package/site/content/docs/elements/ai/models.mdx +283 -0
  20. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  21. package/site/content/docs/elements/channel/email.mdx +266 -0
  22. package/site/content/docs/elements/channel/index.mdx +344 -0
  23. package/site/content/docs/elements/channel/meta.json +5 -0
  24. package/site/content/docs/elements/channel/push.mdx +218 -0
  25. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  26. package/site/content/docs/elements/channel/sms.mdx +264 -0
  27. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  28. package/site/content/docs/elements/clock/index.mdx +300 -0
  29. package/site/content/docs/elements/clock/meta.json +5 -0
  30. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  31. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  32. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  33. package/site/content/docs/elements/flow/http.mdx +1080 -0
  34. package/site/content/docs/elements/flow/index.mdx +618 -0
  35. package/site/content/docs/elements/flow/meta.json +5 -0
  36. package/site/content/docs/elements/flow/routing.mdx +559 -0
  37. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  38. package/site/content/docs/elements/gate/auth.mdx +371 -0
  39. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  40. package/site/content/docs/elements/gate/index.mdx +418 -0
  41. package/site/content/docs/elements/gate/meta.json +5 -0
  42. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  43. package/site/content/docs/elements/gate/rls.mdx +303 -0
  44. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  45. package/site/content/docs/elements/index.mdx +52 -11
  46. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  47. package/site/content/docs/elements/signal/index.mdx +352 -0
  48. package/site/content/docs/elements/signal/live.mdx +590 -0
  49. package/site/content/docs/elements/signal/meta.json +5 -0
  50. package/site/content/docs/elements/signal/once.mdx +596 -0
  51. package/site/content/docs/elements/store/files.mdx +659 -0
  52. package/site/content/docs/elements/store/index.mdx +310 -0
  53. package/site/content/docs/elements/store/kv.mdx +629 -0
  54. package/site/content/docs/elements/store/meta.json +5 -0
  55. package/site/content/docs/elements/store/search.mdx +946 -0
  56. package/site/content/docs/elements/store/sql.mdx +937 -0
  57. package/site/content/docs/elements/vault/config.mdx +256 -0
  58. package/site/content/docs/elements/vault/index.mdx +356 -0
  59. package/site/content/docs/elements/vault/meta.json +5 -0
  60. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  61. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  62. package/site/content/docs/index.mdx +78 -30
  63. package/site/content/docs/meta.json +11 -8
  64. package/site/content/docs/plugins/cors.mdx +2 -1
  65. package/site/content/docs/plugins/csrf.mdx +4 -1
  66. package/site/content/docs/plugins/headers.mdx +2 -1
  67. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  68. package/site/content/docs/plugins/magic-link.mdx +6 -0
  69. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  70. package/site/content/docs/plugins/oauth.mdx +1 -1
  71. package/site/content/docs/plugins/otp.mdx +12 -1
  72. package/site/content/docs/plugins/passkey.mdx +39 -15
  73. package/site/content/docs/plugins/two-factor.mdx +91 -34
  74. package/site/content/docs/plugins/username.mdx +1 -1
  75. package/site/content/docs/recipes/caddy.mdx +3 -3
  76. package/site/content/docs/recipes/index.mdx +18 -28
  77. package/site/content/docs/recipes/mailpit.mdx +1 -1
  78. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  79. package/site/content/docs/recipes/meta.json +1 -4
  80. package/site/content/docs/recipes/nginx.mdx +2 -2
  81. package/site/content/docs/recipes/openrouter.mdx +283 -0
  82. package/site/content/docs/recipes/pgdog.mdx +3 -3
  83. package/site/content/docs/recipes/postgres.mdx +1 -2
  84. package/site/content/docs/recipes/rustfs.mdx +2 -2
  85. package/site/content/docs/recipes/traefik.mdx +3 -3
  86. package/site/content/docs/reference/cli.mdx +231 -0
  87. package/site/content/docs/reference/configuration.mdx +30 -10
  88. package/site/content/docs/reference/environment-variables.mdx +93 -20
  89. package/site/content/docs/reference/errors.mdx +132 -29
  90. package/site/content/docs/reference/fx.mdx +137 -73
  91. package/site/content/docs/reference/index.mdx +13 -21
  92. package/site/content/docs/reference/meta.json +4 -5
  93. package/site/content/docs/reference/okid.mdx +42 -17
  94. package/site/content/docs/reference/plugins.mdx +4 -3
  95. package/site/content/docs/reference/security.mdx +197 -0
  96. package/site/content/docs/understand/meta.json +5 -0
  97. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  98. package/site/content/docs/understand/the-model.mdx +32 -0
  99. package/site/content/docs/understand/the-problem.mdx +74 -0
  100. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  101. package/src/auth/api-keys.ts +2 -1
  102. package/src/auth/bindings.ts +64 -16
  103. package/src/auth/gate-auth.test.ts +6 -1
  104. package/src/auth/identity.ts +152 -5
  105. package/src/auth/index.ts +29 -0
  106. package/src/auth/invites.ts +2 -1
  107. package/src/auth/method-context.ts +41 -0
  108. package/src/auth/oauth-as/crypto.ts +2 -1
  109. package/src/auth/oauth-as/stores.ts +2 -1
  110. package/src/auth/operator.ts +2 -1
  111. package/src/auth/sessions-jwt.test.ts +77 -0
  112. package/src/auth/sessions.ts +20 -4
  113. package/src/auth/tenants.ts +3 -2
  114. package/src/auth/two-factor-challenge.test.ts +61 -0
  115. package/src/auth/two-factor-challenge.ts +284 -0
  116. package/src/auth/verification.ts +5 -0
  117. package/src/bench/REPORT.md +49 -0
  118. package/src/bench/g03-signal-once.bench.ts +1 -1
  119. package/src/bench/g10-observability-contention.bench.ts +1 -1
  120. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  121. package/src/bench/load-app.ts +2 -10
  122. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  123. package/src/cli/ai-setup/apply.ts +294 -52
  124. package/src/cli/ai-setup/catalog.ts +238 -1342
  125. package/src/cli/ai-setup/index.ts +14 -99
  126. package/src/cli/ai-setup/prompts.ts +60 -443
  127. package/src/cli/ask-seed.test.ts +96 -0
  128. package/src/cli/ask-seed.ts +82 -0
  129. package/src/cli/ask-vault-gaps.test.ts +84 -0
  130. package/src/cli/ask-vault-gaps.ts +146 -0
  131. package/src/cli/build.ts +2 -2
  132. package/src/cli/client-add.test.ts +26 -1
  133. package/src/cli/client-add.ts +106 -27
  134. package/src/cli/competitor-mention-removal.test.ts +1 -1
  135. package/src/cli/db-seed.ts +2 -0
  136. package/src/cli/db.ts +150 -11
  137. package/src/cli/dev.ts +119 -211
  138. package/src/cli/docker-clean.ts +2 -2
  139. package/src/cli/docker-cli.test.ts +1 -1
  140. package/src/cli/doctor-diff.ts +4 -2
  141. package/src/cli/doctor-pii.test.ts +1 -1
  142. package/src/cli/doctor.ts +14 -1
  143. package/src/cli/hero-meta.test.ts +4 -2
  144. package/src/cli/load-config.images.test.ts +8 -10
  145. package/src/cli/load-config.ts +1 -1
  146. package/src/cli/registry.ts +22 -10
  147. package/src/cli/replay.ts +3 -1
  148. package/src/cli/start.ts +1 -1
  149. package/src/cli/tui/keys.ts +2 -3
  150. package/src/client/auth/cookies.ts +74 -0
  151. package/src/client/auth/create-auth-client.ts +636 -0
  152. package/src/client/auth/denials.ts +99 -0
  153. package/src/client/auth/session.ts +283 -0
  154. package/src/client/auth.test.ts +251 -0
  155. package/src/client/auth.ts +39 -114
  156. package/src/client/create-with-session.ts +245 -0
  157. package/src/client/create.ts +51 -5
  158. package/src/client/index.ts +11 -1
  159. package/src/client/live.ts +11 -79
  160. package/src/client/notes-contract.test.ts +5 -10
  161. package/src/client/sse.ts +137 -0
  162. package/src/client/stream.ts +147 -0
  163. package/src/client/transport.test.ts +32 -0
  164. package/src/client/transport.ts +123 -26
  165. package/src/client/types.ts +118 -14
  166. package/src/client-react/index.ts +185 -24
  167. package/src/client-react/use-live-query.ts +13 -3
  168. package/src/compiler/aot.test.ts +7 -4
  169. package/src/compiler/effects-embed.test.ts +56 -0
  170. package/src/compiler/effects-fetch.test.ts +43 -0
  171. package/src/compiler/effects-infer.ts +58 -2
  172. package/src/compiler/extract.test.ts +258 -44
  173. package/src/compiler/extract.ts +566 -48
  174. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  175. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  176. package/src/compiler/fixtures/skyport.expected.json +4 -4
  177. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  178. package/src/compiler/response.ts +2 -2
  179. package/src/compiler/search-writer-isolation.test.ts +40 -0
  180. package/src/config/index.ts +11 -0
  181. package/src/console/server/app.ts +17 -23
  182. package/src/console/server/bind.ts +4 -0
  183. package/src/console/server/console.test.ts +2 -2
  184. package/src/console/server/flows-invoke.test.ts +50 -34
  185. package/src/console/server/flows.ts +436 -255
  186. package/src/console/server/runs-ingest.test.ts +6 -3
  187. package/src/console/server/serve.ts +1 -1
  188. package/src/console/server/signals.test.ts +1 -5
  189. package/src/console/server/signals.ts +8 -6
  190. package/src/console/server/state.ts +6 -1
  191. package/src/console/server/store.ts +1 -1
  192. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  193. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  194. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  195. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  196. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  197. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  198. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  199. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  200. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  201. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  202. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  203. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  204. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  205. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  206. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  207. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  208. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  209. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  210. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  211. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  212. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  213. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  214. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  215. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  216. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  217. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  218. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  219. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  220. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  221. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  222. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  223. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  224. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  225. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  226. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  227. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  228. package/src/console/ui-next/dist/index.html +6 -13
  229. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  230. package/src/console/ui-next/src/client.ts +7 -2
  231. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  232. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  233. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  235. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  236. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  237. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  238. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  239. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  240. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  241. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  242. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  243. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  244. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  245. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  246. package/src/docker/ai-model-status.test.ts +1 -12
  247. package/src/docker/ai-model-status.ts +10 -38
  248. package/src/docker/compose-up.test.ts +100 -0
  249. package/src/docker/compose-up.ts +292 -0
  250. package/src/docker/compose.ts +4 -14
  251. package/src/docker/derive.ts +9 -34
  252. package/src/docker/docker.test.ts +16 -261
  253. package/src/docker/images-config.test.ts +13 -25
  254. package/src/docker/index.ts +1 -24
  255. package/src/docker/recipes/index.ts +0 -23
  256. package/src/docker/recipes/pgdog.ts +6 -4
  257. package/src/docker/stack-id.test.ts +2 -2
  258. package/src/docker/stack-id.ts +0 -1
  259. package/src/docker/types.ts +2 -2
  260. package/src/drivers/ai-anthropic.ts +10 -0
  261. package/src/drivers/ai-openai-compatible.ts +34 -7
  262. package/src/drivers/ai-providers.test.ts +0 -107
  263. package/src/drivers/ai-stream.test.ts +0 -34
  264. package/src/drivers/ai-types.ts +17 -8
  265. package/src/drivers/channel-fcm.ts +6 -1
  266. package/src/drivers/channel-msegat.ts +6 -1
  267. package/src/drivers/channel-resend.ts +5 -1
  268. package/src/drivers/channel-smtp.ts +5 -1
  269. package/src/drivers/channel-sndr.ts +10 -1
  270. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  271. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  272. package/src/drivers/channel-taqnyat.ts +6 -1
  273. package/src/drivers/channel-types.ts +10 -0
  274. package/src/drivers/channel-unifonic.ts +6 -1
  275. package/src/drivers/channel-wa-cloud.ts +6 -1
  276. package/src/drivers/channel-webpush.ts +6 -1
  277. package/src/drivers/external.ts +33 -0
  278. package/src/drivers/index.ts +0 -10
  279. package/src/drivers/meilisearch.ts +6 -0
  280. package/src/drivers/oauth-types.ts +4 -0
  281. package/src/drivers/postgres.test.ts +74 -3
  282. package/src/drivers/postgres.ts +175 -6
  283. package/src/drivers/signal-types.ts +5 -5
  284. package/src/drivers/types.ts +7 -0
  285. package/src/drivers/vault-types.ts +5 -0
  286. package/src/elements/ai/declare.ts +17 -4
  287. package/src/elements/ai/eval.ts +1 -1
  288. package/src/elements/ai/mcp-http.ts +1 -1
  289. package/src/elements/ai/pii.ts +1 -1
  290. package/src/elements/ai/providers.test.ts +289 -0
  291. package/src/elements/ai/providers.ts +169 -0
  292. package/src/elements/ai/runtime.ts +68 -5
  293. package/src/elements/ai/schema.ts +2 -2
  294. package/src/elements/ai.test.ts +7 -7
  295. package/src/elements/ai.ts +17 -1
  296. package/src/elements/channel/runtime.ts +105 -12
  297. package/src/elements/clock/cron-fields.test.ts +144 -0
  298. package/src/elements/clock/cron-fields.ts +185 -0
  299. package/src/elements/clock/declare.ts +247 -3
  300. package/src/elements/clock.ts +21 -1
  301. package/src/elements/index.ts +15 -0
  302. package/src/elements/signal/chaos-child.ts +1 -2
  303. package/src/elements/signal/declare.ts +65 -37
  304. package/src/elements/signal/delivery-modes.test.ts +11 -31
  305. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  306. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  307. package/src/elements/signal/key-ordering.test.ts +6 -26
  308. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  309. package/src/elements/signal/optional-emit.test.ts +6 -9
  310. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  311. package/src/elements/signal/orphan-messages.test.ts +4 -15
  312. package/src/elements/signal/reconcile.test.ts +2 -2
  313. package/src/elements/signal/runtime.ts +1 -1
  314. package/src/elements/signal/schema-emit.test.ts +4 -8
  315. package/src/elements/signal.test.ts +24 -49
  316. package/src/elements/signal.ts +9 -2
  317. package/src/elements/store/domain-ddl.test.ts +1 -1
  318. package/src/elements/store/live-http.test.ts +9 -4
  319. package/src/elements/store/prepare-row.test.ts +123 -0
  320. package/src/elements/store/resource.ts +43 -12
  321. package/src/elements/store/schema-decl.ts +117 -0
  322. package/src/elements/store/search-backfill.ts +205 -0
  323. package/src/elements/store/search-bind.ts +66 -0
  324. package/src/elements/store/search-bm25.ts +78 -0
  325. package/src/elements/store/search-ddl.ts +156 -0
  326. package/src/elements/store/search-embed-flow.ts +141 -0
  327. package/src/elements/store/search-errors.ts +34 -0
  328. package/src/elements/store/search-fusion.ts +112 -0
  329. package/src/elements/store/search-lsh.ts +179 -0
  330. package/src/elements/store/search-runtime.ts +300 -0
  331. package/src/elements/store/search.test.ts +144 -0
  332. package/src/elements/store/sql-session.test.ts +1 -0
  333. package/src/elements/store/sql-session.ts +83 -3
  334. package/src/elements/store/table.ts +23 -2
  335. package/src/elements/store/upsert-app.test.ts +1 -3
  336. package/src/elements/store.ts +50 -0
  337. package/src/full.ts +4 -2
  338. package/src/http.ts +4 -1
  339. package/src/i18n/catalogs/ar.ts +14 -6
  340. package/src/i18n/catalogs/en.ts +14 -6
  341. package/src/index.ts +6 -1
  342. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  343. package/src/kernel/adopt-routes.ts +9 -1
  344. package/src/kernel/app-auth.ts +5 -0
  345. package/src/kernel/app.ts +102 -9
  346. package/src/kernel/auto-cache.test.ts +6 -12
  347. package/src/kernel/auto-registry.test.ts +3 -3
  348. package/src/kernel/boot-bind/ai.test.ts +22 -19
  349. package/src/kernel/boot-bind/ai.ts +5 -43
  350. package/src/kernel/boot-bind/clock.ts +23 -5
  351. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  352. package/src/kernel/boot.test.ts +11 -6
  353. package/src/kernel/boot.ts +66 -16
  354. package/src/kernel/boundary-contract.ts +93 -0
  355. package/src/kernel/budget.test.ts +1 -1
  356. package/src/kernel/call.ts +89 -0
  357. package/src/kernel/capability.ts +6 -0
  358. package/src/kernel/client-descriptor.ts +123 -0
  359. package/src/kernel/clock-timezone.test.ts +80 -0
  360. package/src/kernel/correlation.test.ts +1 -1
  361. package/src/kernel/dry-run.ts +11 -8
  362. package/src/kernel/effects-stamping.test.ts +42 -9
  363. package/src/kernel/effects.test.ts +4 -2
  364. package/src/kernel/effects.ts +51 -9
  365. package/src/kernel/errors-channel.ts +17 -0
  366. package/src/kernel/errors-live-resume.ts +3 -2
  367. package/src/kernel/errors-tenant.ts +7 -4
  368. package/src/kernel/errors.registry-helpers.ts +96 -0
  369. package/src/kernel/errors.registry.test.ts +139 -25
  370. package/src/kernel/errors.ts +102 -23
  371. package/src/kernel/external-effects.test.ts +196 -0
  372. package/src/kernel/flow.test.ts +13 -4
  373. package/src/kernel/flow.ts +69 -71
  374. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  375. package/src/kernel/fx-dead-letters.test.ts +3 -3
  376. package/src/kernel/fx-fetch.ts +55 -0
  377. package/src/kernel/fx-live-stream.ts +2 -2
  378. package/src/kernel/fx-live.test.ts +8 -8
  379. package/src/kernel/fx.test.ts +17 -11
  380. package/src/kernel/fx.ts +180 -57
  381. package/src/kernel/horizontal-child.ts +1 -1
  382. package/src/kernel/http-query.test.ts +3 -6
  383. package/src/kernel/index.ts +13 -3
  384. package/src/kernel/instance-id.ts +1 -1
  385. package/src/kernel/instances.test.ts +40 -0
  386. package/src/kernel/instances.ts +47 -9
  387. package/src/kernel/live-http.test.ts +6 -7
  388. package/src/kernel/live-http.ts +6 -2
  389. package/src/kernel/live-resume.test.ts +1 -1
  390. package/src/kernel/mcp-tool.test.ts +3 -3
  391. package/src/kernel/on.ts +98 -4
  392. package/src/kernel/pipeline.test.ts +12 -15
  393. package/src/kernel/plugin-elements.test.ts +1 -1
  394. package/src/kernel/stamp-http.test.ts +3 -3
  395. package/src/kernel/triggers.ts +210 -95
  396. package/src/kernel-entry.ts +0 -1
  397. package/src/manifest/diff.ts +11 -1
  398. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  399. package/src/manifest/types.ts +46 -2
  400. package/src/mcp/docs-index.ts +3 -3
  401. package/src/mcp/docs-mcp.test.ts +2 -2
  402. package/src/mcp/docs-tools.ts +1 -1
  403. package/src/okid.test.ts +56 -0
  404. package/src/okid.ts +55 -11
  405. package/src/plugins/anonymous.ts +8 -4
  406. package/src/plugins/auth/shared.ts +27 -3
  407. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  408. package/src/plugins/auth-methods.security.test.ts +561 -23
  409. package/src/plugins/config-source.ts +1 -1
  410. package/src/plugins/csrf.test.ts +65 -0
  411. package/src/plugins/index.ts +2 -0
  412. package/src/plugins/magic-link.ts +36 -16
  413. package/src/plugins/oauth/shared.ts +3 -1
  414. package/src/plugins/oauth.ts +22 -14
  415. package/src/plugins/otp.ts +37 -21
  416. package/src/plugins/passkey-webauthn.ts +9 -6
  417. package/src/plugins/passkey.ts +98 -26
  418. package/src/plugins/pre-account-hijack.test.ts +223 -0
  419. package/src/plugins/two-factor.ts +466 -37
  420. package/src/plugins/username.ts +19 -7
  421. package/src/release/build-lib.ts +1 -0
  422. package/src/release/limits.ts +2 -2
  423. package/src/release/measure.ts +1 -1
  424. package/src/runs/collect.test.ts +0 -2
  425. package/src/runtime/json-code-block.test.ts +250 -20
  426. package/src/runtime/json-code-block.ts +1261 -41
  427. package/src/term.test.ts +1 -1
  428. package/src/test/create-test-app.test.ts +12 -12
  429. package/src/test/live-signals.test.ts +6 -6
  430. package/src/test/provisions.integration.test.ts +10 -14
  431. package/src/test/reset-element-registries.ts +1 -1
  432. package/src/test/tenant-isolation.test.ts +12 -6
  433. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  434. package/site/content/docs/deployment/docker.mdx +0 -227
  435. package/site/content/docs/deployment/index.mdx +0 -83
  436. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  437. package/site/content/docs/deployment/meta.json +0 -5
  438. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  439. package/site/content/docs/elements/ai.mdx +0 -385
  440. package/site/content/docs/elements/channel.mdx +0 -346
  441. package/site/content/docs/elements/clock.mdx +0 -244
  442. package/site/content/docs/elements/flow.mdx +0 -420
  443. package/site/content/docs/elements/gate.mdx +0 -436
  444. package/site/content/docs/elements/signal.mdx +0 -380
  445. package/site/content/docs/elements/store.mdx +0 -1099
  446. package/site/content/docs/elements/vault.mdx +0 -405
  447. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  448. package/site/content/docs/get-started/index.mdx +0 -43
  449. package/site/content/docs/get-started/installation.mdx +0 -220
  450. package/site/content/docs/get-started/introduction.mdx +0 -132
  451. package/site/content/docs/get-started/meta.json +0 -13
  452. package/site/content/docs/get-started/project-structure.mdx +0 -925
  453. package/site/content/docs/get-started/testing.mdx +0 -328
  454. package/site/content/docs/get-started/why.mdx +0 -162
  455. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  456. package/site/content/docs/recipes/ollama.mdx +0 -142
  457. package/site/content/docs/recipes/sglang.mdx +0 -105
  458. package/site/content/docs/recipes/vllm.mdx +0 -106
  459. package/site/content/docs/reference/cli.md +0 -232
  460. package/site/content/docs/reference/client.mdx +0 -469
  461. package/site/content/docs/reference/security.md +0 -74
  462. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  463. package/src/cli/ai-setup/recommend.test.ts +0 -225
  464. package/src/cli/ai-setup/recommend.ts +0 -225
  465. package/src/cli/dev-controls.test.ts +0 -122
  466. package/src/cli/dev-controls.ts +0 -164
  467. package/src/cli/tui/DevLive.tsx +0 -126
  468. package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
  469. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  470. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  471. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  472. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  473. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  474. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  475. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  476. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  477. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  478. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  479. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  480. package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
  481. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  482. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  483. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  484. package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
  485. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  486. package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
  487. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  488. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  489. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  490. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  491. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  492. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  493. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  494. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  495. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  496. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  497. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  498. package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
  499. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  500. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  501. package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
  502. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  503. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  504. package/src/docker/ollama-pull.ts +0 -232
  505. package/src/docker/recipes/llama-cpp.ts +0 -298
  506. package/src/docker/recipes/ollama.ts +0 -44
  507. package/src/docker/recipes/sglang.ts +0 -55
  508. package/src/docker/recipes/vllm.ts +0 -44
  509. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  510. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  511. package/src/drivers/ai-ollama.ts +0 -389
  512. 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>