vybekiit 0.7.32 → 0.7.34

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 (80) hide show
  1. package/dist/bin.js +303 -85
  2. package/dist/global-skills/aws-serverless/SKILL.md +168 -32
  3. package/dist/global-skills/aws-serverless/references/api-gateway.md +17 -0
  4. package/dist/global-skills/aws-serverless/references/architecture.md +9 -13
  5. package/dist/global-skills/aws-serverless/references/lambda.md +3 -3
  6. package/dist/global-skills/aws-serverless/references/orchestration.md +46 -20
  7. package/dist/global-skills/aws-serverless/references/production.md +1 -1
  8. package/dist/global-skills/eas-simulator/SKILL.md +15 -15
  9. package/dist/global-skills/eas-simulator/references/controllers.md +11 -7
  10. package/dist/global-skills/eas-simulator/references/run-your-app.md +8 -8
  11. package/dist/global-skills/eas-simulator/references/troubleshooting.md +10 -6
  12. package/dist/global-skills/email-best-practices/references/sending-reliability.md +17 -22
  13. package/dist/global-skills/extension-to-functions-codebase/SKILL.md +1 -0
  14. package/dist/global-skills/firebase-ai-logic-basics/SKILL.md +161 -76
  15. package/dist/global-skills/firebase-ai-logic-basics/references/flutter_setup.md +37 -0
  16. package/dist/global-skills/firebase-ai-logic-basics/references/ios_setup.md +97 -3
  17. package/dist/global-skills/firebase-ai-logic-basics/references/usage_patterns_android.md +88 -3
  18. package/dist/global-skills/firebase-ai-logic-basics/references/usage_patterns_web.md +91 -3
  19. package/dist/global-skills/firebase-app-hosting-basics/SKILL.md +5 -1
  20. package/dist/global-skills/firebase-auth-basics/SKILL.md +1 -0
  21. package/dist/global-skills/firebase-auth-basics/references/client_sdk_android.md +9 -9
  22. package/dist/global-skills/firebase-auth-basics/references/client_sdk_web.md +3 -3
  23. package/dist/global-skills/firebase-auth-basics/references/flutter_setup.md +22 -19
  24. package/dist/global-skills/firebase-auth-basics/references/security_rules.md +2 -2
  25. package/dist/global-skills/firebase-basics/SKILL.md +6 -2
  26. package/dist/global-skills/firebase-basics/references/android_setup.md +101 -37
  27. package/dist/global-skills/firebase-basics/references/ios_setup.md +57 -0
  28. package/dist/global-skills/firebase-basics/references/web_setup.md +52 -0
  29. package/dist/global-skills/firebase-crashlytics/SKILL.md +1 -0
  30. package/dist/global-skills/firebase-crashlytics/references/android_setup.md +84 -84
  31. package/dist/global-skills/firebase-crashlytics/references/ios_setup.md +1 -0
  32. package/dist/global-skills/firebase-data-connect/SKILL.md +1 -0
  33. package/dist/global-skills/firebase-data-connect/reference/config.md +8 -8
  34. package/dist/global-skills/firebase-data-connect/reference/realtime.md +4 -1
  35. package/dist/global-skills/firebase-data-connect/reference/sdk_android.md +33 -34
  36. package/dist/global-skills/firebase-data-connect/reference/sdk_web.md +8 -3
  37. package/dist/global-skills/firebase-data-connect/reference/security.md +13 -13
  38. package/dist/global-skills/firebase-firestore/SKILL.md +17 -7
  39. package/dist/global-skills/firebase-firestore/references/enterprise/android_sdk_usage.md +32 -32
  40. package/dist/global-skills/firebase-firestore/references/enterprise/indexes.md +2 -1
  41. package/dist/global-skills/firebase-firestore/references/standard/android_sdk_usage.md +14 -14
  42. package/dist/global-skills/firebase-firestore/references/standard/indexes.md +2 -1
  43. package/dist/global-skills/firebase-hosting-basics/SKILL.md +1 -0
  44. package/dist/global-skills/firebase-remote-config-basics/SKILL.md +2 -1
  45. package/dist/global-skills/firebase-remote-config-basics/references/android_setup.md +36 -37
  46. package/dist/global-skills/firebase-security-rules-auditor/SKILL.md +7 -6
  47. package/dist/global-skills/firestore-rules-creation/SKILL.md +22 -10
  48. package/dist/global-skills/manifest.json +2 -1
  49. package/dist/global-skills/neon/SKILL.md +31 -10
  50. package/dist/global-skills/neon-ai-gateway/SKILL.md +43 -14
  51. package/dist/global-skills/neon-functions/SKILL.md +12 -11
  52. package/dist/global-skills/neon-functions/references/sse.md +2 -2
  53. package/dist/global-skills/neon-object-storage/SKILL.md +5 -4
  54. package/dist/global-skills/neon-postgres/SKILL.md +5 -2
  55. package/dist/global-skills/neon-postgres/references/full-text-search.md +27 -17
  56. package/dist/global-skills/neon-postgres/references/hybrid-search.md +19 -15
  57. package/dist/global-skills/neon-postgres/references/lakebase-search-drizzle.md +99 -14
  58. package/dist/global-skills/neon-postgres/references/vector-search.md +93 -10
  59. package/dist/global-skills/neon-realtime/SKILL.md +207 -0
  60. package/dist/global-skills/neon-realtime/references/mutations.md +743 -0
  61. package/dist/global-skills/neon-realtime/references/queries.md +509 -0
  62. package/dist/global-skills/neon-realtime/references/reactivity.md +407 -0
  63. package/dist/global-skills/neon-realtime/references/ssr.md +695 -0
  64. package/dist/global-skills/neon-realtime/references/subscribing.md +485 -0
  65. package/dist/global-skills/resend/SKILL.md +7 -16
  66. package/dist/global-skills/resend/references/domains.md +1 -1
  67. package/dist/global-skills/resend/references/inboxes.md +349 -0
  68. package/dist/global-skills/resend/references/installation.md +3 -3
  69. package/dist/global-skills/resend/references/sending/best-practices.md +3 -3
  70. package/dist/global-skills/resend/references/sending/single-email-examples.md +3 -3
  71. package/dist/global-skills/resend/references/webhooks.md +9 -0
  72. package/dist/global-skills/stripe-best-practices/SKILL.md +18 -18
  73. package/dist/global-skills/stripe-best-practices/references/tax.md +26 -14
  74. package/dist/global-skills/use-railway/SKILL.md +6 -6
  75. package/dist/global-skills/use-railway/references/iac.md +2 -1
  76. package/dist/global-skills/use-railway/references/tracing.md +13 -5
  77. package/dist/global-skills/workers-best-practices/SKILL.md +1 -1
  78. package/dist/global-skills/wrangler/SKILL.md +3 -3
  79. package/dist/global-skills/xcode-project-setup/SKILL.md +1 -0
  80. package/package.json +8 -8
@@ -79,7 +79,7 @@ export default app;
79
79
 
80
80
  ## Push to every client, across isolates
81
81
 
82
- The fan-out rule is identical to WebSockets ([Keeping clients in sync across isolates](../SKILL.md#keeping-clients-in-sync-across-isolates-do-not-skip-this)): each isolate keeps its **own** set of open streams, so broadcasting in-process only reaches the clients on that isolate. Hold a `Set` of stream controllers and pick a strategy there — **poll Postgres** by default (keeps Scale to Zero), or `LISTEN`/`NOTIFY` (shown below) for lowest latency on always-on compute. Keep the source-of-truth state in Postgres — module state doesn't survive eviction.
82
+ If the data is database rows (live dashboards, feeds, chat over stored messages), use Neon Realtime (the `neon-realtime` skill) instead of hand-rolling fan-out. Otherwise, the fan-out rule is identical to WebSockets ([Fan-out across isolates](../SKILL.md#fan-out-across-isolates-do-not-skip-this)): each isolate keeps its **own** set of open streams, so broadcasting in-process only reaches the clients on that isolate. Hold a `Set` of stream controllers and pick a strategy there — **poll Postgres** by default (keeps Scale to Zero), or `LISTEN`/`NOTIFY` (shown below) for lowest latency on always-on compute. Keep the source-of-truth state in Postgres — module state doesn't survive eviction.
83
83
 
84
84
  ```typescript
85
85
  import { attachDatabasePool } from "@neon/functions";
@@ -160,4 +160,4 @@ source.onerror = () => {
160
160
  - **CORS.** A SPA hits the function cross-origin, so set `Access-Control-Allow-Origin`. `EventSource` sends no credentials by default, so `*` is fine for public streams.
161
161
  - **One-way only.** SSE is server → client. For client → server, the browser makes normal `fetch`/`POST` calls (often to the same function); reach for [WebSockets](../SKILL.md#websocket-servers) only when you need bidirectional, low-latency frames.
162
162
 
163
- Together — a Hono `fetch` SSE endpoint, cross-isolate fan-out, heartbeat, a counter persisted in Postgres, and a client-only TanStack Router SPA consuming it with `EventSource` — these compose into a complete realtime backend on a single function.
163
+ Together — a Hono `fetch` SSE endpoint, cross-isolate fan-out, heartbeat, a counter persisted in Postgres, and a client-only TanStack Router SPA consuming it with `EventSource` — these compose into a complete push backend on a single function, for push that doesn't come from database rows (presence, typing, LLM streams) or where Realtime isn't available. For persisted chat, feeds, and other live UIs over database rows, use Neon Realtime (the `neon-realtime` skill).
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  name: neon-object-storage
3
3
  description: >-
4
- S3-compatible object storage that branches with your Neon project, so files
5
- and the database stay in sync across every branch. Use when a user wants
4
+ S3-compatible object storage that branches with your database, so each
5
+ branch gets its own copy of the files and the rows that point at them.
6
+ Use when a user wants
6
7
  object storage, a bucket, blob/file storage, or somewhere to put uploads,
7
8
  images, documents, avatars, or user-generated files for their app or agent —
8
9
  especially when they already use (or are setting up) Lakebase Postgres and don't
@@ -30,7 +31,7 @@ neon skills -s neon -y
30
31
 
31
32
  Currently available in `aws-us-east-2`, `aws-us-east-1`, `aws-eu-central-1`, and `aws-ap-southeast-1`.
32
33
 
33
- Neon Object Storage is S3-compatible object storage that branches with your projects: every branch gets its own isolated storage state, so files and database rows stay in sync across dev, preview, staging, and production.
34
+ Neon Object Storage is S3-compatible file storage built into your Neon project. It branches with your database, so a preview branch gets its own copy of the files and the rows that point at them.
34
35
 
35
36
  Use this skill to help the user store and serve files that branch alongside their database. Deliver a working bucket and upload/download flow, a branch-aware S3 client wired to the injected env vars, or a precise answer from the official Neon docs.
36
37
 
@@ -39,7 +40,7 @@ Use this skill to help the user store and serve files that branch alongside thei
39
40
  Reach for Neon Object Storage for the files an app and its users produce — uploads, attachments, avatars, images, documents, generated assets, backups. It is the default place to put them when the app is already on Neon:
40
41
 
41
42
  - **They already use Lakebase Postgres and don't want a second provider.** One backend, one bill, one CLI, one set of branches — instead of standing up and wiring a separate AWS S3 / R2 / Supabase Storage account. The same Neon credential that backs the database backs storage.
42
- - **Files must stay in sync with the database across environments.** Storage branches _together with_ your Postgres data. Fork a branch and the child instantly inherits the parent's buckets and objects at that point in time — copy-on-write, so no data is duplicated. This is what makes agent, dev, preview, and test environments seamless: a preview branch gets a consistent snapshot of _both_ the rows and the files they reference, and writes on the child never touch the parent.
43
+ - **Files must branch with the database across environments.** Storage branches _together with_ your Postgres data. Fork a branch and the child instantly inherits the parent's buckets and objects at that point in time — copy-on-write, so no data is duplicated. This is what makes agent, dev, preview, and test environments seamless: a preview branch gets a consistent snapshot of _both_ the rows and the files they reference, and writes on the child never touch the parent.
43
44
  - **They want safe, throwaway environments.** Upload, overwrite, and delete files in a preview/CI branch without any risk to production data, then drop the branch.
44
45
  - **They want standard S3 tooling.** It's built on S3 semantics and speaks the S3 API, so the AWS SDKs, `boto3`, the AWS CLI, and presigned URLs all work — reliable and familiar, with no proprietary client.
45
46
 
@@ -6,7 +6,8 @@ description: >-
6
6
  scale-to-zero, instant restore, read replicas, IP allow lists, logical
7
7
  replication, and Lakebase Search. Use when the work is an existing
8
8
  DATABASE_URL, SQL, schema, inspect, or search. New backends, Auth, files,
9
- Functions, and LLM calls go to the parent `neon` skill. Also use for
9
+ Functions, and LLM calls go to the parent `neon` skill. Live queries, reactive
10
+ UIs, and realtime data go to `neon-realtime`. Also use for
10
11
  "@neondatabase/serverless", "@neondatabase/neon-js", "neon inspect db",
11
12
  "semantic search", "vector search", "full-text search", "BM25", or
12
13
  "hybrid search".
@@ -32,6 +33,8 @@ It is the same database whether you reach it through Neon or through Databricks;
32
33
 
33
34
  Login, users, sessions, and `@neondatabase/auth` belong in `neon-auth`.
34
35
 
36
+ Live queries, reactive UIs, and realtime data belong in `neon-realtime`.
37
+
35
38
  ## Setup Flow
36
39
 
37
40
  ### 1. Select the organization and project
@@ -258,7 +261,7 @@ Link: https://neon.com/docs/guides/logical-replication-guide.md
258
261
 
259
262
  Use Lakebase Search for semantic, full-text, and hybrid search:
260
263
 
261
- - For semantic search, read [Vector search](references/vector-search.md).
264
+ - For semantic search, including generating embeddings with the Neon AI Gateway, read [Vector search](references/vector-search.md).
262
265
  - For full-text search with BM25 ranking, read [Full-text search](references/full-text-search.md).
263
266
  - For combining semantic and lexical results, read [Hybrid search](references/hybrid-search.md).
264
267
  - For managing any of the above through Drizzle ORM, read [Managing Lakebase Search with Drizzle](references/lakebase-search-drizzle.md).
@@ -45,18 +45,24 @@ After a large bulk load, run `VACUUM` to refresh the statistics used by BM25 sco
45
45
  `to_bm25query` binds the query `tsvector` to the BM25 index whose corpus statistics should be used. The `<@>` operator returns a negative BM25 score, so lower (more negative) values are more relevant and must sort ascending:
46
46
 
47
47
  ```sql
48
- SELECT
49
- id,
50
- title,
51
- body_tsv <@> to_bm25query(
52
- to_tsvector('english', $1),
53
- 'documents_body_bm25'::regclass
54
- ) AS score
55
- FROM documents
48
+ SELECT id, title, score
49
+ FROM (
50
+ SELECT
51
+ id,
52
+ title,
53
+ body_tsv <@> to_bm25query(
54
+ to_tsvector('english', $1),
55
+ 'documents_body_bm25'::regclass
56
+ ) AS score
57
+ FROM documents
58
+ ) AS keyword_scored
59
+ WHERE score < 0
56
60
  ORDER BY score
57
61
  LIMIT $2;
58
62
  ```
59
63
 
64
+ Scalar BM25 scoring returns zero for documents containing none of the query terms. Filter `score < 0` before `ORDER BY` and `LIMIT` so keyword results exclude non-matches under both sequential and index plans. Partial query-term matches remain eligible.
65
+
60
66
  Use the same text-search configuration for document and query vectors. Select a language-specific or custom configuration that matches the corpus.
61
67
 
62
68
  ## Set the Candidate Limit
@@ -72,15 +78,19 @@ BEGIN;
72
78
  SET LOCAL lakebase_bm25.default_limit = 20;
73
79
  SET LOCAL lakebase_bm25.prefilter = on;
74
80
 
75
- SELECT
76
- id,
77
- title,
78
- body_tsv <@> to_bm25query(
79
- to_tsvector('english', $1),
80
- 'documents_body_bm25'::regclass
81
- ) AS score
82
- FROM documents
83
- WHERE id % 1000 = 0
81
+ SELECT id, title, score
82
+ FROM (
83
+ SELECT
84
+ id,
85
+ title,
86
+ body_tsv <@> to_bm25query(
87
+ to_tsvector('english', $1),
88
+ 'documents_body_bm25'::regclass
89
+ ) AS score
90
+ FROM documents
91
+ WHERE id % 1000 = 0
92
+ ) AS keyword_scored
93
+ WHERE score < 0
84
94
  ORDER BY score
85
95
  LIMIT $2;
86
96
  COMMIT;
@@ -18,13 +18,13 @@ CREATE TABLE documents (
18
18
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
19
19
  title text NOT NULL,
20
20
  body text NOT NULL,
21
- embedding vector(1536),
21
+ embedding vector(1024),
22
22
  body_tsv tsvector GENERATED ALWAYS AS
23
23
  (to_tsvector('english', body)) STORED
24
24
  );
25
25
  ```
26
26
 
27
- Replace `1536` with the embedding model's dimension. Use the same model and preprocessing for stored-document and query embeddings, and choose a PostgreSQL text-search configuration appropriate for the corpus.
27
+ The 1024-dimensional column is an example. Set it to the output dimension of your selected embedding model. Generate document embeddings in batches and embed each query with the same model and dimensions; [Vector search](vector-search.md#prepare-embeddings) shows the AI Gateway calls. Follow any model-specific query instructions, while keeping the original query text for BM25. Choose a PostgreSQL text-search configuration appropriate for the corpus.
28
28
 
29
29
  Create and validate each retriever independently before combining them. Follow [Vector search](vector-search.md) and [Full-text search](full-text-search.md) for their indexes, query operators, and tuning.
30
30
 
@@ -34,7 +34,7 @@ Reciprocal Rank Fusion (RRF) is the approach in the Lakebase Search get-started
34
34
 
35
35
  For rank `r` and constant `k`, each retriever contributes `1 / (k + r)`. The documented starting point uses 40 candidates per retriever and `k = 60`; tune both for the corpus and workload.
36
36
 
37
- Bind the query embedding as `$1`, query text as `$2`, and final result count as `$3`:
37
+ Embed the query text through the same gateway model used for the stored documents, then bind that vector as `$1`, the original query text as `$2`, and the final result count as `$3`:
38
38
 
39
39
  ```sql
40
40
  WITH vector_ranked AS (
@@ -49,15 +49,19 @@ WITH vector_ranked AS (
49
49
  keyword_ranked AS (
50
50
  SELECT id, RANK() OVER (ORDER BY score) AS rank
51
51
  FROM (
52
- SELECT
53
- id,
54
- body_tsv <@> to_bm25query(
55
- to_tsvector('english', $2),
56
- 'documents_body_bm25'::regclass
57
- ) AS score
58
- FROM documents
59
- ORDER BY score
60
- FETCH FIRST 40 ROWS WITH TIES
52
+ SELECT id, score
53
+ FROM (
54
+ SELECT
55
+ id,
56
+ body_tsv <@> to_bm25query(
57
+ to_tsvector('english', $2),
58
+ 'documents_body_bm25'::regclass
59
+ ) AS score
60
+ FROM documents
61
+ ) AS keyword_scored
62
+ WHERE score < 0
63
+ ORDER BY score, id
64
+ LIMIT 40
61
65
  ) AS keyword_candidates
62
66
  )
63
67
  SELECT
@@ -73,13 +77,13 @@ ORDER BY rrf_score DESC, d.id
73
77
  LIMIT $3;
74
78
  ```
75
79
 
76
- `RANK()` gives tied retrieval scores the same rank. Sort by `rrf_score` descending and use the stable ID as a final tie-breaker.
80
+ `score < 0` excludes BM25 non-matches. Many matching documents can share a BM25 score, so `WITH TIES` could admit a large group at the cutoff. `ORDER BY score, id LIMIT 40` keeps keyword candidates bounded and breaks boundary ties by ID. `RANK()` uses score alone so retained ties receive equal ranks.
77
81
 
78
- `FETCH FIRST ... ROWS WITH TIES` keeps every candidate tied at the cutoff, so `RANK()` receives the complete boundary tie group. The candidate set can therefore exceed 40 rows. `lakebase_bm25.default_limit` defaults to `1000`; increase it only when the BM25 candidate set needs to exceed that value.
82
+ Vector retrieval preserves cutoff ties and can exceed 40 candidates. `lakebase_bm25.default_limit` defaults to `1000`; the SQL limit bounds keyword rows entering fusion, not total scoring work.
79
83
 
80
84
  ## Adapt the Hybrid Search
81
85
 
82
- - Retrieve more candidates from each source than the final result count; otherwise one retriever can dominate before fusion has enough overlap. Keep `lakebase_bm25.default_limit` above the BM25 candidate target and allow room for boundary ties.
86
+ - Retrieve more candidates from each source than the final result count; otherwise one retriever can dominate before fusion has enough overlap. Keep `lakebase_bm25.default_limit` at least as large as the BM25 candidate target.
83
87
  - Keep each retriever's operator and index configuration correct independently before tuning RRF.
84
88
  - Tune candidate counts and the RRF constant with judged or behavioral relevance data, plus latency measurements.
85
89
  - Add weights only when product evidence shows one retriever should contribute more. Weight the reciprocal-rank contributions, not the raw vector distance and negative BM25 score.
@@ -10,6 +10,7 @@ Contents:
10
10
  - [Extensions](#extensions): custom migration required to create extensions (Drizzle can't)
11
11
  - [Schema](#schema): columns, generated `tsvector`, and the ANN index
12
12
  - [BM25 Index](#bm25-index): created after the corpus is seeded
13
+ - [Generate and Store Embeddings](#generate-and-store-embeddings): call the AI Gateway and insert vectors with Drizzle
13
14
  - [Query](#query): vector, BM25, and hybrid reads
14
15
  - [Tune Per Query](#tune-per-query): per-query GUCs
15
16
 
@@ -22,18 +23,22 @@ Rules:
22
23
 
23
24
  ## Config
24
25
 
25
- `drizzle-kit generate` and `migrate` read `drizzle.config.ts`. Point `dbCredentials.url` at the **direct (unpooled)** connection string:
26
+ `drizzle-kit generate` and `migrate` read `drizzle.config.ts`. If the connection string is in `.env`, install `dotenv` (`npm install dotenv`) and load it here. Point `dbCredentials.url` at the **direct (unpooled)** connection string:
26
27
 
27
28
  ```typescript
28
29
  // drizzle.config.ts
30
+ import "dotenv/config";
29
31
  import { defineConfig } from "drizzle-kit";
30
32
 
33
+ const databaseUrl = process.env.DATABASE_URL_UNPOOLED;
34
+ if (!databaseUrl) throw new Error("Missing DATABASE_URL_UNPOOLED");
35
+
31
36
  export default defineConfig({
32
37
  schema: "./src/schema.ts",
33
38
  out: "./drizzle",
34
39
  dialect: "postgresql",
35
40
  // Direct (unpooled) URL. Neon exposes it as DATABASE_URL_UNPOOLED.
36
- dbCredentials: { url: process.env.DATABASE_URL_UNPOOLED },
41
+ dbCredentials: { url: databaseUrl },
37
42
  });
38
43
  ```
39
44
 
@@ -57,7 +62,14 @@ The columns, the generated `tsvector`, and the `lakebase_ann` index all go in `s
57
62
 
58
63
  ```typescript
59
64
  // src/schema.ts
60
- import { pgTable, bigint, text, vector, index, customType } from "drizzle-orm/pg-core";
65
+ import {
66
+ pgTable,
67
+ bigint,
68
+ text,
69
+ vector,
70
+ index,
71
+ customType,
72
+ } from "drizzle-orm/pg-core";
61
73
  import { sql } from "drizzle-orm";
62
74
 
63
75
  const tsvector = customType<{ data: string }>({
@@ -69,10 +81,12 @@ const tsvector = customType<{ data: string }>({
69
81
  export const documents = pgTable(
70
82
  "documents",
71
83
  {
72
- id: bigint("id", { mode: "number" }).generatedByDefaultAsIdentity().primaryKey(),
84
+ id: bigint("id", { mode: "number" })
85
+ .generatedByDefaultAsIdentity()
86
+ .primaryKey(),
73
87
  title: text("title").notNull(),
74
88
  body: text("body").notNull(),
75
- embedding: vector("embedding", { dimensions: 1536 }),
89
+ embedding: vector("embedding", { dimensions: 1024 }),
76
90
  bodyTsv: tsvector("body_tsv").generatedAlwaysAs(
77
91
  sql`to_tsvector('english', "body")`,
78
92
  ),
@@ -86,7 +100,7 @@ export const documents = pgTable(
86
100
  );
87
101
  ```
88
102
 
89
- Set the dimension to match your embedding model. Postgres maintains `body_tsv`, so never write it from the app. Generate and apply the migration after the extensions migration above:
103
+ The 1024-dimensional column is an example; set it to the output dimension of your selected embedding model. Use the same model and dimensions for stored documents and queries. Postgres maintains `body_tsv`, so never write it from the app. Generate and apply the migration after the extensions migration above:
90
104
 
91
105
  ```bash
92
106
  npx drizzle-kit generate --name=lakebase_search
@@ -106,6 +120,66 @@ npx drizzle-kit generate --custom --name=bm25_index
106
120
  CREATE INDEX documents_body_bm25 ON documents USING lakebase_bm25 (body_tsv);
107
121
  ```
108
122
 
123
+ ## Generate and Store Embeddings
124
+
125
+ Use the [Vercel AI SDK](https://ai-sdk.dev) (`ai`, 6+) with Neon's `@neon/ai-sdk-provider` 1.0.0+. Follow the [gateway setup](vector-search.md#prepare-embeddings), then install the packages with `npm install @neon/ai-sdk-provider ai dotenv`:
126
+
127
+ ```typescript
128
+ import "dotenv/config";
129
+ import { neon } from "@neon/ai-sdk-provider";
130
+ import { embed, embedMany } from "ai";
131
+
132
+ const embeddingModel = neon.embeddingModel("qwen3-embedding-0-6b");
133
+ const providerOptions = { neon: { dimensions: 1024 } };
134
+ // Qwen3 query prompt adapted for Neon documentation search; documents stay unprefixed.
135
+ const queryPrompt =
136
+ "Instruct: Given a question about Neon, retrieve documentation passages that answer the question\nQuery: ";
137
+ ```
138
+
139
+ Generate a document vector, then insert it with Drizzle:
140
+
141
+ ```typescript
142
+ const { embedding } = await embed({
143
+ model: embeddingModel,
144
+ value: body,
145
+ providerOptions,
146
+ });
147
+ await db.insert(documents).values({
148
+ title,
149
+ body,
150
+ embedding,
151
+ });
152
+ ```
153
+
154
+ For bulk imports, skip blank rows and embed/insert one batch at a time:
155
+
156
+ ```typescript
157
+ const batchSize = 64;
158
+ const nonBlankRows = rows.filter((row) => row && row.body.trim().length > 0);
159
+
160
+ for (let offset = 0; offset < nonBlankRows.length; offset += batchSize) {
161
+ const batch = nonBlankRows.slice(offset, offset + batchSize);
162
+ const { embeddings: vectors } = await embedMany({
163
+ model: embeddingModel,
164
+ values: batch.map((row) => row.body),
165
+ providerOptions,
166
+ maxParallelCalls: 1,
167
+ });
168
+ await db.insert(documents).values(
169
+ batch.map((row, index) => {
170
+ const embedding = vectors[index];
171
+ if (!embedding)
172
+ throw new Error(`Missing embedding for batch row ${index}`);
173
+ return { ...row, embedding };
174
+ }),
175
+ );
176
+ }
177
+ ```
178
+
179
+ `embedMany` splits embedding requests using the provider's per-request input limit. This loop also bounds database inserts and memory; tune `batchSize` for your workload. Chunk long documents before embedding. Earlier batches stay committed if a later batch fails.
180
+
181
+ Apply `queryPrompt` only to queries, keeping the original text for BM25. Use the same model and dimensions for documents and queries; re-embed documents when switching models.
182
+
109
183
  ## Query
110
184
 
111
185
  Use the query builder with Drizzle's `cosineDistance` helper for vector search. It emits the `<=>` operator, so keep the index on `vector_cosine_ops`:
@@ -114,7 +188,11 @@ Use the query builder with Drizzle's `cosineDistance` helper for vector search.
114
188
  import { cosineDistance } from "drizzle-orm";
115
189
  import { documents } from "./schema";
116
190
 
117
- // queryEmbedding: number[] from the same model used for stored documents
191
+ const { embedding: queryEmbedding } = await embed({
192
+ model: embeddingModel,
193
+ value: `${queryPrompt}${queryText}`,
194
+ providerOptions,
195
+ });
118
196
  const distance = cosineDistance(documents.embedding, queryEmbedding);
119
197
 
120
198
  const rows = await db
@@ -130,17 +208,23 @@ BM25 has no Drizzle helper: `<@>` and `to_bm25query` require raw SQL. Reference
130
208
  import { sql } from "drizzle-orm";
131
209
 
132
210
  const rows = await db.execute(sql`
133
- SELECT id, title,
134
- body_tsv <@> to_bm25query(
135
- to_tsvector('english', ${queryText}),
136
- 'documents_body_bm25'::regclass
137
- ) AS score
138
- FROM documents
211
+ SELECT id, title, score
212
+ FROM (
213
+ SELECT id, title,
214
+ body_tsv <@> to_bm25query(
215
+ to_tsvector('english', ${queryText}),
216
+ 'documents_body_bm25'::regclass
217
+ ) AS score
218
+ FROM documents
219
+ ) AS keyword_scored
220
+ WHERE score < 0
139
221
  ORDER BY score
140
222
  LIMIT ${k}
141
223
  `);
142
224
  ```
143
225
 
226
+ Filter `score < 0` before the candidate cutoff: scalar BM25 scoring returns zero for non-matches, which must not become keyword hits. Partial query-term matches remain eligible.
227
+
144
228
  Run the [hybrid search](hybrid-search.md) RRF query the same way: raw SQL through `db.execute`.
145
229
 
146
230
  ## Tune Per Query
@@ -168,5 +252,6 @@ const rows = await db.transaction(async (tx) => {
168
252
 
169
253
  Sources:
170
254
 
255
+ - [AI Gateway embeddings](https://neon.com/docs/ai-gateway/embeddings)
171
256
  - [Get started with Lakebase Search](https://neon.com/docs/ai/lakebase-search-get-started)
172
- - [Schema migration with Lakebase Postgres and Drizzle ORM](https://neon.com/docs/guides/drizzle-migrations)
257
+ - [Schema migration with Lakebase Postgres and Drizzle ORM](https://neon.com/docs/guides/drizzle-migrations)
@@ -5,7 +5,7 @@ Use `lakebase_vector` for approximate nearest-neighbor retrieval over embeddings
5
5
  ## Contents
6
6
 
7
7
  - [Create the extension](#create-the-extension) — enable `lakebase_vector` and its `pgvector` dependency
8
- - [Prepare embeddings](#prepare-embeddings) — define the vector column and keep embedding dimensions consistent
8
+ - [Prepare embeddings](#prepare-embeddings) — generate vectors with the AI Gateway and match the column dimension
9
9
  - [Build the index](#build-the-index) — match the distance metric, operator class, and query operator
10
10
  - [Tune the index](#tune-the-index) — configure index-build options and concurrent index management
11
11
  - [Query](#query) — rank by vector distance or filter by a similarity radius
@@ -24,28 +24,111 @@ CREATE EXTENSION IF NOT EXISTS lakebase_vector CASCADE;
24
24
 
25
25
  ## Prepare Embeddings
26
26
 
27
- Use any embedding provider whose vector dimensions and distance metric match the schema and index:
27
+ Neon's [AI Gateway embeddings endpoint](https://neon.com/docs/ai-gateway/embeddings) is an OpenAI-compatible way to generate vectors for Lakebase Search. This table uses 1024 dimensions as an example; set the column dimension to the output of the model and any `dimensions` parameter you choose:
28
28
 
29
29
  ```sql
30
30
  CREATE TABLE documents (
31
31
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
32
32
  title text NOT NULL,
33
33
  body text NOT NULL,
34
- embedding vector(1536)
34
+ embedding vector(1024)
35
35
  );
36
36
  ```
37
37
 
38
- Replace `1536` with the embedding model's dimension. Generate stored-document and query embeddings with the same model and preprocessing. Keep embedding generation outside SQL unless the architecture already provides an in-database embedding function.
38
+ Enable the AI Gateway on the branch and get `NEON_AI_GATEWAY_TOKEN` and the bare-host `NEON_AI_GATEWAY_BASE_URL`:
39
+
40
+ - With `neon.ts`, declare `aiGateway: true`, run `neon deploy`, then run `neon env pull --file .env`.
41
+ - Without `neon.ts`, run `neon env pull --service ai-gateway --file .env` from a directory linked to the intended project and branch. This requests the gateway variables explicitly and skips other services.
42
+ - Alternatively, get both values from the Neon Console. Neon Functions receive them automatically.
43
+
44
+ ### AI SDK (Recommended)
45
+
46
+ For TypeScript apps, use Neon's `@neon/ai-sdk-provider` 1.0.0+ with the [Vercel AI SDK](https://ai-sdk.dev) (`ai`, 6+) (`npm install @neon/ai-sdk-provider ai dotenv`). The provider reads `NEON_AI_GATEWAY_TOKEN` and `NEON_AI_GATEWAY_BASE_URL` directly and sends embedding requests to `/v1/embeddings`. Load `.env` in a standalone app; Neon Functions inject the variables automatically.
47
+
48
+ ```typescript
49
+ import "dotenv/config";
50
+ import { neon } from "@neon/ai-sdk-provider";
51
+ import { embed, embedMany } from "ai";
52
+
53
+ const embeddingModel = neon.embeddingModel("qwen3-embedding-0-6b");
54
+ const providerOptions = { neon: { dimensions: 1024 } };
55
+ // Qwen3 query prompt adapted for Neon documentation search; documents stay unprefixed.
56
+ const queryPrompt =
57
+ "Instruct: Given a question about Neon, retrieve documentation passages that answer the question\nQuery: ";
58
+
59
+ const { embeddings: documentEmbeddings } = await embedMany({
60
+ model: embeddingModel,
61
+ values: documentBodies, // non-empty document batch; store vectors by input index
62
+ providerOptions,
63
+ maxParallelCalls: 1,
64
+ });
65
+ const { embedding: queryEmbedding } = await embed({
66
+ model: embeddingModel,
67
+ value: `${queryPrompt}${queryText}`,
68
+ providerOptions,
69
+ }); // bind to $1 below
70
+ ```
71
+
72
+ Skip empty imports before calling `embedMany`. It returns vectors in input order and splits requests according to the provider's item limit. For large imports, also split document text within the endpoint's token limits and keep database writes bounded.
73
+
74
+ For Neon documentation search, the example adapts the task instruction in Qwen3's stored `query` prompt from [config_sentence_transformers.json](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B/blob/main/config_sentence_transformers.json). Its `document` prompt is empty, so document text stays unprefixed. Neither SDK applies SentenceTransformers prompts automatically. Keep the original `queryText` for BM25, and use the selected model's own prompt conventions if you switch models.
75
+
76
+ ### OpenAI SDK Alternative
77
+
78
+ For an existing OpenAI SDK integration, install `openai` and `dotenv` (`npm install openai dotenv`) and configure the client explicitly:
79
+
80
+ ```typescript
81
+ import "dotenv/config";
82
+ import OpenAI from "openai";
83
+
84
+ const apiKey = process.env.NEON_AI_GATEWAY_TOKEN;
85
+ const gatewayBaseUrl = process.env.NEON_AI_GATEWAY_BASE_URL;
86
+ if (!apiKey || !gatewayBaseUrl) {
87
+ throw new Error("Missing Neon AI Gateway credentials");
88
+ }
89
+
90
+ const client = new OpenAI({
91
+ apiKey,
92
+ baseURL: `${gatewayBaseUrl}/v1`,
93
+ });
94
+
95
+ async function embedBatch(inputs: string[]): Promise<number[][]> {
96
+ if (inputs.length === 0) return [];
97
+ const response = await client.embeddings.create({
98
+ model: "qwen3-embedding-0-6b",
99
+ input: inputs,
100
+ dimensions: 1024,
101
+ encoding_format: "float",
102
+ });
103
+ const vectors: number[][] = [];
104
+ for (const item of response.data) vectors[item.index] = item.embedding;
105
+ return inputs.map((_, index) => {
106
+ const embedding = vectors[index];
107
+ if (!embedding) throw new Error(`Missing embedding for input ${index}`);
108
+ return embedding;
109
+ });
110
+ }
111
+
112
+ const documentEmbeddings = await embedBatch(documentBodies);
113
+ const [queryEmbedding] = await embedBatch([`${queryPrompt}${queryText}`]);
114
+ if (!queryEmbedding) throw new Error("Missing query embedding");
115
+ ```
116
+
117
+ Use the same `queryPrompt` defined above, only for query inputs. Keep each batch within the endpoint's input and rate limits, and pair each vector with its source row using the response `index`. The [OpenAI JavaScript SDK v6](https://github.com/openai/openai-node/blob/v6.0.0/src/resources/embeddings.ts) defaults to requesting base64 and decodes the response as base64. If a compatible endpoint returns float arrays instead, that decoder can silently turn them into zeros. Pass `encoding_format: "float"` explicitly for the AI Gateway; the SDK then returns the float arrays directly. Base64 can reduce response size when an endpoint actually returns base64, but do not assume that format for this gateway.
118
+
119
+ Use the same model and output dimensions for stored documents and queries. Follow the model's input conventions: some models recommend a query instruction while document text remains unprefixed. If the model supports shorter vectors, compare retrieval quality, storage, and latency before changing dimensions; set the column and both embedding requests to the chosen size. Switching models requires re-embedding the corpus; changing dimensions also requires migrating the vector column and index. Generate embeddings in the application, outside SQL, unless the architecture already provides an in-database embedding function. Other embedding providers are also valid when their output matches the schema and index.
39
120
 
40
121
  ## Build the Index
41
122
 
42
123
  Choose the operator class and query operator as a matched pair:
43
124
 
44
- | Metric | Common use | Operator class | Distance operator |
45
- | --- | --- | --- | --- |
46
- | Cosine | Most text embeddings | `vector_cosine_ops` | `<=>` |
47
- | L2 / Euclidean | Absolute distance matters; vectors do not need normalization | `vector_l2_ops` | `<->` |
48
- | Inner product | Unit-normalized vectors; matches cosine for unit vectors | `vector_ip_ops` | `<#>` |
125
+ | Metric | Common use | Operator class | Distance operator |
126
+ | -------------- | ------------------------------------------------------------ | ------------------- | ----------------- |
127
+ | Cosine | Most text embeddings | `vector_cosine_ops` | `<=>` |
128
+ | L2 / Euclidean | Absolute distance matters; vectors do not need normalization | `vector_l2_ops` | `<->` |
129
+ | Inner product | Unit-normalized vectors; matches cosine for unit vectors | `vector_ip_ops` | `<#>` |
130
+
131
+ Choose the metric recommended for the selected model, and keep its index operator class and query operator aligned. The [Qwen3 Embedding model card](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B) demonstrates cosine similarity and normalized dot product for retrieval. For unit-normalized vectors, cosine, inner product, and L2 have the same exact ranking; approximate indexes can still differ in recall and latency, so benchmark on your corpus.
49
132
 
50
133
  ```sql
51
134
  CREATE INDEX documents_embedding_ann ON documents
@@ -79,7 +162,7 @@ For a large table, use `CREATE INDEX CONCURRENTLY` to avoid locking out writes w
79
162
 
80
163
  ## Query
81
164
 
82
- Generate the query embedding with the same model and preprocessing used for stored documents, then bind it as a parameter:
165
+ Generate the query embedding with the same model and dimensions used for stored documents, following the model's query-input conventions (using the Qwen3 query prompt shown above), then bind it as a parameter:
83
166
 
84
167
  ```sql
85
168
  SELECT id, title, embedding <=> $1::vector AS distance