sonamu 0.8.24 → 0.8.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/__tests__/config.test.js +189 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +7 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +14 -10
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/knex-adapter.d.ts +23 -0
- package/dist/auth/knex-adapter.d.ts.map +1 -0
- package/dist/auth/knex-adapter.js +163 -0
- package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
- package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
- package/dist/bin/cli.js +47 -9
- package/dist/bin/ts-loader-register.js +3 -29
- package/dist/bin/ts-loader-registration.d.ts +2 -0
- package/dist/bin/ts-loader-registration.d.ts.map +1 -0
- package/dist/bin/ts-loader-registration.js +42 -0
- package/dist/cone/cone-generator.js +3 -3
- package/dist/database/puri-subset.test-d.js +9 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/testing/fixture-generator.js +5 -5
- package/dist/ui/ai-client.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -14
- package/dist/ui/cdd-service.d.ts +15 -18
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +246 -222
- package/dist/ui/cdd-types.d.ts +41 -68
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +2 -2
- package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
- package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
- package/dist/ui-web/index.html +2 -2
- package/package.json +6 -2
- package/src/api/__tests__/config.test.ts +225 -0
- package/src/api/config.ts +10 -4
- package/src/api/sonamu.ts +16 -13
- package/src/auth/index.ts +1 -0
- package/src/auth/knex-adapter.ts +208 -0
- package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
- package/src/bin/cli.ts +52 -9
- package/src/bin/ts-loader-register.ts +2 -32
- package/src/bin/ts-loader-registration.ts +55 -0
- package/src/cone/cone-generator.ts +2 -2
- package/src/database/puri-subset.test-d.ts +102 -0
- package/src/database/puri-subset.types.ts +1 -1
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +179 -137
- package/src/skills/sonamu/ai-agents.md +69 -69
- package/src/skills/sonamu/api.md +147 -147
- package/src/skills/sonamu/auth-migration.md +220 -220
- package/src/skills/sonamu/auth-plugins.md +83 -83
- package/src/skills/sonamu/auth.md +106 -106
- package/src/skills/sonamu/cdd.md +65 -200
- package/src/skills/sonamu/cone.md +138 -138
- package/src/skills/sonamu/config.md +191 -191
- package/src/skills/sonamu/create-sonamu.md +66 -66
- package/src/skills/sonamu/database.md +158 -158
- package/src/skills/sonamu/entity-basic.md +292 -293
- package/src/skills/sonamu/entity-relations.md +246 -246
- package/src/skills/sonamu/entity-validation-checklist.md +124 -124
- package/src/skills/sonamu/fixture-cli.md +231 -231
- package/src/skills/sonamu/framework-change.md +37 -37
- package/src/skills/sonamu/frontend.md +223 -223
- package/src/skills/sonamu/i18n.md +82 -82
- package/src/skills/sonamu/migration.md +77 -77
- package/src/skills/sonamu/model.md +222 -222
- package/src/skills/sonamu/naite.md +86 -86
- package/src/skills/sonamu/project-init.md +228 -228
- package/src/skills/sonamu/puri.md +122 -122
- package/src/skills/sonamu/scaffolding.md +154 -154
- package/src/skills/sonamu/skill-contribution.md +124 -124
- package/src/skills/sonamu/subset.md +46 -46
- package/src/skills/sonamu/tasks.md +82 -82
- package/src/skills/sonamu/testing-devrunner.md +147 -147
- package/src/skills/sonamu/testing.md +673 -673
- package/src/skills/sonamu/upsert.md +79 -79
- package/src/skills/sonamu/vector.md +67 -67
- package/src/testing/fixture-generator.ts +4 -4
- package/src/ui/ai-client.ts +1 -1
- package/src/ui/api.ts +18 -17
- package/src/ui/cdd-service.ts +264 -254
- package/src/ui/cdd-types.ts +40 -75
- package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
- package/src/skills/sonamu/workflow.md +0 -317
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sonamu-puri
|
|
3
|
-
description: Sonamu Puri
|
|
3
|
+
description: Sonamu Puri type-safe query builder. SELECT, WHERE, JOIN, aggregate functions, FTS, vector search, transactions. Use when writing database queries in Model.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Puri
|
|
6
|
+
# Puri Query Builder
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Starting a Query
|
|
9
9
|
|
|
10
10
|
```typescript
|
|
11
|
-
//
|
|
11
|
+
// Read
|
|
12
12
|
const users = await this.getPuri("r").table("users").select({ id: "id", name: "username" });
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Write
|
|
15
15
|
await this.getPuri("w").table("users").where("id", 1).update({ is_active: false });
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// Using aliases
|
|
18
18
|
const users = await db.table({ u: "users" }).select({ id: "u.id" });
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## SELECT
|
|
22
22
|
|
|
23
|
-
> **CRITICAL: `.select()
|
|
23
|
+
> **CRITICAL: `.select()` must always be used with an object argument.**
|
|
24
24
|
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
25
|
+
> Passing a string argument causes the string to be spread character-by-character, generating incorrect SQL like `select "i" as "0", "d" as "1", ...`.
|
|
26
|
+
> Be especially careful when chaining after an `as any` cast.
|
|
27
27
|
>
|
|
28
28
|
> ```typescript
|
|
29
|
-
> // WRONG — character spread
|
|
29
|
+
> // WRONG — character spread bug
|
|
30
30
|
> db.table("files").select("files.entity_id", "files.file_type")
|
|
31
31
|
>
|
|
32
32
|
> // CORRECT
|
|
@@ -34,13 +34,13 @@ const users = await db.table({ u: "users" }).select({ id: "u.id" });
|
|
|
34
34
|
> ```
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
//
|
|
37
|
+
// Basic select
|
|
38
38
|
const users = await db.table("users").select({ id: "id", name: "username" });
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// All columns
|
|
41
41
|
const users = await db.table("users").selectAll();
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// Nested objects (auto-converted during hydration)
|
|
44
44
|
db.select({
|
|
45
45
|
id: "users.id",
|
|
46
46
|
parent: {
|
|
@@ -49,13 +49,13 @@ db.select({
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Append to existing select
|
|
53
53
|
db.select({ id: "id" }).appendSelect({ name: "username" });
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## Static
|
|
56
|
+
## Static Functions (for SELECT)
|
|
57
57
|
|
|
58
|
-
###
|
|
58
|
+
### Aggregate Functions
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
61
61
|
// COUNT
|
|
@@ -71,7 +71,7 @@ db.select({
|
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
###
|
|
74
|
+
### String Functions
|
|
75
75
|
|
|
76
76
|
```typescript
|
|
77
77
|
db.select({
|
|
@@ -81,12 +81,12 @@ db.select({
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
### Raw SQL
|
|
84
|
+
### Raw SQL Expressions
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Bind parameters can be passed as the second argument `params`. Do not interpolate values directly into SQL; use params instead.
|
|
87
87
|
|
|
88
88
|
```typescript
|
|
89
|
-
//
|
|
89
|
+
// Without parameters
|
|
90
90
|
db.select({
|
|
91
91
|
custom: Puri.rawString("COALESCE(nickname, username)"),
|
|
92
92
|
total: Puri.rawNumber("price * quantity"),
|
|
@@ -95,26 +95,26 @@ db.select({
|
|
|
95
95
|
tags: Puri.rawStringArray("string_to_array(tags, ',')"),
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
// params
|
|
98
|
+
// Bind with params array (prevents SQL injection)
|
|
99
99
|
db.select({
|
|
100
100
|
score: Puri.rawNumber(
|
|
101
101
|
`word_similarity(?, items.title) * 5 + word_similarity(?, items.tags) * 2`,
|
|
102
102
|
[query, query]
|
|
103
103
|
),
|
|
104
|
-
label: Puri.rawString(`COALESCE(??, ?)`, ["items.name", "
|
|
104
|
+
label: Puri.rawString(`COALESCE(??, ?)`, ["items.name", "Unspecified"]),
|
|
105
105
|
});
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
## WHERE
|
|
109
109
|
|
|
110
110
|
```typescript
|
|
111
|
-
//
|
|
111
|
+
// Basic
|
|
112
112
|
db.where("role", "admin")
|
|
113
113
|
db.where("age", ">=", 18)
|
|
114
114
|
db.where("deleted_at", null) // IS NULL
|
|
115
115
|
db.where("deleted_at", "!=", null) // IS NOT NULL
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Multiple conditions (AND)
|
|
118
118
|
db.where("role", "admin").where("is_active", true)
|
|
119
119
|
|
|
120
120
|
// IN / NOT IN
|
|
@@ -128,7 +128,7 @@ db.where("email", "like", `%${keyword}%`)
|
|
|
128
128
|
db.whereRaw("EXTRACT(YEAR FROM created_at) = ?", [2024])
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
### WHERE
|
|
131
|
+
### WHERE Grouping (Parentheses)
|
|
132
132
|
|
|
133
133
|
```typescript
|
|
134
134
|
// (role = 'admin' OR role = 'moderator') AND is_active = true
|
|
@@ -136,7 +136,7 @@ db.whereGroup((g) => {
|
|
|
136
136
|
g.where("role", "admin").orWhere("role", "moderator");
|
|
137
137
|
}).where("is_active", true);
|
|
138
138
|
|
|
139
|
-
// OR
|
|
139
|
+
// OR group
|
|
140
140
|
db.where("status", "active")
|
|
141
141
|
.orWhereGroup((g) => {
|
|
142
142
|
g.where("role", "admin").where("is_verified", true);
|
|
@@ -155,19 +155,19 @@ db.table("employees")
|
|
|
155
155
|
db.table("employees")
|
|
156
156
|
.leftJoin("departments", "employees.department_id", "departments.id")
|
|
157
157
|
|
|
158
|
-
//
|
|
158
|
+
// Using aliases
|
|
159
159
|
db.table({ e: "employees" })
|
|
160
160
|
.join({ u: "users" }, "e.user_id", "u.id")
|
|
161
161
|
.leftJoin({ d: "departments" }, "e.department_id", "d.id")
|
|
162
162
|
|
|
163
|
-
//
|
|
163
|
+
// Complex JOIN conditions with callback
|
|
164
164
|
db.table("orders")
|
|
165
165
|
.join("products", (j) => {
|
|
166
166
|
j.on("orders.product_id", "products.id")
|
|
167
167
|
.on("orders.store_id", "products.store_id");
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// Subquery JOIN
|
|
171
171
|
const subquery = db.table("order_items")
|
|
172
172
|
.select({ order_id: "order_id", total: Puri.sum("amount") })
|
|
173
173
|
.groupBy("order_id");
|
|
@@ -182,7 +182,7 @@ db.table("orders")
|
|
|
182
182
|
```typescript
|
|
183
183
|
db.orderBy("created_at", "desc")
|
|
184
184
|
.limit(20)
|
|
185
|
-
.offset(40) // 3
|
|
185
|
+
.offset(40) // Page 3
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
## GROUP BY & HAVING
|
|
@@ -197,14 +197,14 @@ db.table("orders")
|
|
|
197
197
|
.groupBy("user_id")
|
|
198
198
|
.having("COUNT(*) > 5");
|
|
199
199
|
|
|
200
|
-
//
|
|
200
|
+
// Column, operator, value form
|
|
201
201
|
db.groupBy("user_id").having("count", ">", 10);
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
## INSERT
|
|
205
205
|
|
|
206
206
|
```typescript
|
|
207
|
-
//
|
|
207
|
+
// Basic INSERT
|
|
208
208
|
await db.table("users").insert({ username: "john", email: "john@test.com" });
|
|
209
209
|
|
|
210
210
|
// RETURNING
|
|
@@ -212,12 +212,12 @@ const [{ id }] = await db.table("users")
|
|
|
212
212
|
.insert({ username: "john" })
|
|
213
213
|
.returning("id");
|
|
214
214
|
|
|
215
|
-
//
|
|
215
|
+
// Multiple columns RETURNING
|
|
216
216
|
const [row] = await db.table("users")
|
|
217
217
|
.insert({ username: "john" })
|
|
218
218
|
.returning(["id", "created_at"]);
|
|
219
219
|
|
|
220
|
-
//
|
|
220
|
+
// All columns RETURNING
|
|
221
221
|
const [user] = await db.table("users")
|
|
222
222
|
.insert({ username: "john" })
|
|
223
223
|
.returning("*");
|
|
@@ -229,14 +229,14 @@ const [user] = await db.table("users")
|
|
|
229
229
|
// DO NOTHING
|
|
230
230
|
await db.table("users")
|
|
231
231
|
.insert({ id: 1, username: "john" })
|
|
232
|
-
.onConflict("id"); //
|
|
232
|
+
.onConflict("id"); // or .onConflict("id", "nothing")
|
|
233
233
|
|
|
234
|
-
// DO UPDATE -
|
|
234
|
+
// DO UPDATE - specific columns only
|
|
235
235
|
await db.table("users")
|
|
236
236
|
.insert({ id: 1, username: "john", email: "new@test.com" })
|
|
237
237
|
.onConflict("id", { update: ["username", "email"] });
|
|
238
238
|
|
|
239
|
-
// DO UPDATE -
|
|
239
|
+
// DO UPDATE - with specified values
|
|
240
240
|
await db.table("users")
|
|
241
241
|
.insert({ id: 1, username: "john" })
|
|
242
242
|
.onConflict("id", {
|
|
@@ -246,7 +246,7 @@ await db.table("users")
|
|
|
246
246
|
}
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
//
|
|
249
|
+
// Composite key conflict
|
|
250
250
|
await db.table("user_settings")
|
|
251
251
|
.insert({ user_id: 1, key: "theme", value: "dark" })
|
|
252
252
|
.onConflict(["user_id", "key"], { update: ["value"] });
|
|
@@ -268,13 +268,13 @@ await db.table("users").where("id", 1).decrement("credit", 100);
|
|
|
268
268
|
await db.table("users").where("id", 1).delete();
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
##
|
|
271
|
+
## Result Methods
|
|
272
272
|
|
|
273
|
-
|
|
|
274
|
-
|
|
275
|
-
| `await query` | `T[]` |
|
|
276
|
-
| `first()` | `Promise<T \| undefined>` |
|
|
277
|
-
| `pluck("col")` | `Promise<V[]>` |
|
|
273
|
+
| Method | Returns | Description |
|
|
274
|
+
|--------|---------|-------------|
|
|
275
|
+
| `await query` | `T[]` | Array result (Puri is Thenable) |
|
|
276
|
+
| `first()` | `Promise<T \| undefined>` | First record |
|
|
277
|
+
| `pluck("col")` | `Promise<V[]>` | Array of a specific column only |
|
|
278
278
|
|
|
279
279
|
```typescript
|
|
280
280
|
const users = await db.table("users").select({ id: "id" }); // T[]
|
|
@@ -282,31 +282,31 @@ const user = await db.table("users").where("id", 1).first(); // T | und
|
|
|
282
282
|
const ids = await db.table("users").where("role", "admin").pluck("id"); // number[]
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
-
##
|
|
285
|
+
## Utilities
|
|
286
286
|
|
|
287
287
|
```typescript
|
|
288
|
-
//
|
|
288
|
+
// Inspect query string
|
|
289
289
|
const sql = db.table("users").where("id", 1).toQuery();
|
|
290
290
|
|
|
291
|
-
//
|
|
291
|
+
// Debug log output (prints query to console, then continues chaining)
|
|
292
292
|
await db.table("users").where("id", 1).debug().first();
|
|
293
293
|
|
|
294
|
-
//
|
|
294
|
+
// Clone a query
|
|
295
295
|
const baseQuery = db.table("users").where("is_active", true);
|
|
296
296
|
const query1 = baseQuery.clone().where("role", "admin");
|
|
297
297
|
const query2 = baseQuery.clone().where("role", "user");
|
|
298
298
|
|
|
299
|
-
//
|
|
300
|
-
db.clear("select") // SELECT
|
|
301
|
-
db.clear("order") // ORDER BY
|
|
302
|
-
db.clear("limit") // LIMIT
|
|
303
|
-
db.clear("offset") // OFFSET
|
|
299
|
+
// Clear parts of a query
|
|
300
|
+
db.clear("select") // Clear SELECT clause
|
|
301
|
+
db.clear("order") // Clear ORDER BY
|
|
302
|
+
db.clear("limit") // Clear LIMIT
|
|
303
|
+
db.clear("offset") // Clear OFFSET
|
|
304
304
|
|
|
305
|
-
//
|
|
305
|
+
// Remove a specific JOIN
|
|
306
306
|
db.clearJoin("alias")
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
##
|
|
309
|
+
## Transactions
|
|
310
310
|
|
|
311
311
|
```typescript
|
|
312
312
|
await this.getPuri("w").transaction(async (trx) => {
|
|
@@ -323,30 +323,30 @@ await this.getPuri("w").transaction(async (trx) => {
|
|
|
323
323
|
### whereTsSearch
|
|
324
324
|
|
|
325
325
|
```typescript
|
|
326
|
-
//
|
|
327
|
-
db.whereTsSearch("search_vector", "
|
|
326
|
+
// Basic search (websearch_to_tsquery, simple config)
|
|
327
|
+
db.whereTsSearch("search_vector", "search term")
|
|
328
328
|
|
|
329
|
-
// config
|
|
330
|
-
db.whereTsSearch("search_vector", "
|
|
329
|
+
// Specify config
|
|
330
|
+
db.whereTsSearch("search_vector", "search term", "korean")
|
|
331
331
|
|
|
332
|
-
//
|
|
333
|
-
db.whereTsSearch("search_vector", "
|
|
332
|
+
// Detailed options
|
|
333
|
+
db.whereTsSearch("search_vector", "search term", {
|
|
334
334
|
parser: "plainto_tsquery", // websearch_to_tsquery | plainto_tsquery | phraseto_tsquery
|
|
335
335
|
config: "korean",
|
|
336
336
|
})
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
### tsHighlight (
|
|
339
|
+
### tsHighlight (Search Term Highlighting)
|
|
340
340
|
|
|
341
341
|
```typescript
|
|
342
342
|
db.select({
|
|
343
343
|
title: "title",
|
|
344
|
-
highlighted: Puri.tsHighlight("content", "
|
|
344
|
+
highlighted: Puri.tsHighlight("content", "search term"),
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
//
|
|
347
|
+
// Options
|
|
348
348
|
db.select({
|
|
349
|
-
highlighted: Puri.tsHighlight("content", "
|
|
349
|
+
highlighted: Puri.tsHighlight("content", "search term", {
|
|
350
350
|
config: "korean",
|
|
351
351
|
startSel: "<mark>",
|
|
352
352
|
stopSel: "</mark>",
|
|
@@ -357,39 +357,39 @@ db.select({
|
|
|
357
357
|
});
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
### tsRank / tsRankCd (
|
|
360
|
+
### tsRank / tsRankCd (Search Ranking)
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
The first argument must be `Puri.toTsVector()` rather than a column name string.
|
|
363
363
|
|
|
364
364
|
```typescript
|
|
365
|
-
// toTsVector()
|
|
365
|
+
// Must wrap with toTsVector() (required)
|
|
366
366
|
db.select({
|
|
367
|
-
rank: Puri.tsRank(Puri.toTsVector("documents.search_vector"), "
|
|
367
|
+
rank: Puri.tsRank(Puri.toTsVector("documents.search_vector"), "search term"),
|
|
368
368
|
})
|
|
369
|
-
.whereTsSearch("documents.search_vector", "
|
|
369
|
+
.whereTsSearch("documents.search_vector", "search term")
|
|
370
370
|
.orderBy("rank", "desc");
|
|
371
371
|
|
|
372
|
-
// config
|
|
372
|
+
// Specify config
|
|
373
373
|
db.select({
|
|
374
|
-
rank: Puri.tsRank(Puri.toTsVector("documents.title", "korean"), "
|
|
374
|
+
rank: Puri.tsRank(Puri.toTsVector("documents.title", "korean"), "search term"),
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
//
|
|
377
|
+
// Options
|
|
378
378
|
db.select({
|
|
379
|
-
rank: Puri.tsRank(Puri.toTsVector("documents.title"), "
|
|
380
|
-
normalization: 1, //
|
|
381
|
-
weights: [0.1, 0.2, 0.4, 1.0], // D, C, B, A
|
|
379
|
+
rank: Puri.tsRank(Puri.toTsVector("documents.title"), "search term", {
|
|
380
|
+
normalization: 1, // Document length normalization
|
|
381
|
+
weights: [0.1, 0.2, 0.4, 1.0], // D, C, B, A weights
|
|
382
382
|
}),
|
|
383
383
|
});
|
|
384
384
|
|
|
385
385
|
// tsRankCd (Cover Density)
|
|
386
386
|
db.select({
|
|
387
|
-
rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "
|
|
387
|
+
rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "search term"),
|
|
388
388
|
});
|
|
389
389
|
|
|
390
|
-
// tsRankCd
|
|
390
|
+
// tsRankCd options
|
|
391
391
|
db.select({
|
|
392
|
-
rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "
|
|
392
|
+
rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "search term", {
|
|
393
393
|
parser: "phraseto_tsquery",
|
|
394
394
|
normalization: 16,
|
|
395
395
|
}),
|
|
@@ -403,19 +403,19 @@ db.select({
|
|
|
403
403
|
### whereSearch
|
|
404
404
|
|
|
405
405
|
```typescript
|
|
406
|
-
//
|
|
407
|
-
db.whereSearch("title", "
|
|
406
|
+
// Single column search
|
|
407
|
+
db.whereSearch("title", "search term")
|
|
408
408
|
|
|
409
|
-
//
|
|
410
|
-
db.whereSearch(["title", "content"], "
|
|
409
|
+
// Multi-column search (requires same column composition as index)
|
|
410
|
+
db.whereSearch(["title", "content"], "search term")
|
|
411
411
|
|
|
412
|
-
//
|
|
413
|
-
db.whereSearch(["title", "content"], "
|
|
414
|
-
weights: [10, 1], //
|
|
412
|
+
// Weight options
|
|
413
|
+
db.whereSearch(["title", "content"], "search term", {
|
|
414
|
+
weights: [10, 1], // 10x weight on title
|
|
415
415
|
})
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
-
### score (
|
|
418
|
+
### score (Search Score)
|
|
419
419
|
|
|
420
420
|
```typescript
|
|
421
421
|
db.select({
|
|
@@ -423,26 +423,26 @@ db.select({
|
|
|
423
423
|
title: "title",
|
|
424
424
|
score: Puri.score(),
|
|
425
425
|
})
|
|
426
|
-
.whereSearch("title", "
|
|
426
|
+
.whereSearch("title", "search term")
|
|
427
427
|
.orderBy("score", "desc");
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
-
### highlight (
|
|
430
|
+
### highlight (Highlighting)
|
|
431
431
|
|
|
432
432
|
```typescript
|
|
433
|
-
//
|
|
433
|
+
// Single column
|
|
434
434
|
db.select({
|
|
435
|
-
highlighted: Puri.highlight("title", "
|
|
435
|
+
highlighted: Puri.highlight("title", "search term"),
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
//
|
|
438
|
+
// Multiple columns (returns array)
|
|
439
439
|
db.select({
|
|
440
|
-
highlighted: Puri.highlight(["title", "content"], "
|
|
440
|
+
highlighted: Puri.highlight(["title", "content"], "search term"),
|
|
441
441
|
});
|
|
442
442
|
|
|
443
|
-
//
|
|
443
|
+
// Array of search terms
|
|
444
444
|
db.select({
|
|
445
|
-
highlighted: Puri.highlight("title", ["
|
|
445
|
+
highlighted: Puri.highlight("title", ["term1", "term2"]),
|
|
446
446
|
});
|
|
447
447
|
```
|
|
448
448
|
|
|
@@ -453,9 +453,9 @@ db.select({
|
|
|
453
453
|
### vectorSimilarity
|
|
454
454
|
|
|
455
455
|
```typescript
|
|
456
|
-
const embedding = await getEmbedding("
|
|
456
|
+
const embedding = await getEmbedding("search query");
|
|
457
457
|
|
|
458
|
-
//
|
|
458
|
+
// Default (cosine similarity)
|
|
459
459
|
const results = await db.table("documents")
|
|
460
460
|
.select({ id: "id", title: "title" })
|
|
461
461
|
.vectorSimilarity("embedding", embedding);
|
|
@@ -468,39 +468,39 @@ db.vectorSimilarity("embedding", embedding, { method: "l2" });
|
|
|
468
468
|
// Inner product
|
|
469
469
|
db.vectorSimilarity("embedding", embedding, { method: "inner_product" });
|
|
470
470
|
|
|
471
|
-
// threshold
|
|
471
|
+
// threshold filter
|
|
472
472
|
db.vectorSimilarity("embedding", embedding, {
|
|
473
473
|
method: "cosine",
|
|
474
|
-
threshold: 0.7, // similarity >= 0.7
|
|
474
|
+
threshold: 0.7, // only similarity >= 0.7
|
|
475
475
|
});
|
|
476
476
|
|
|
477
|
-
// distinctOn (
|
|
477
|
+
// distinctOn (deduplicate)
|
|
478
478
|
db.vectorSimilarity("embedding", embedding, {
|
|
479
|
-
distinctOn: "document_id", // document_id
|
|
479
|
+
distinctOn: "document_id", // best similarity per document_id only
|
|
480
480
|
});
|
|
481
481
|
```
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
**Return value**: `similarity` column is automatically added
|
|
484
484
|
|
|
485
|
-
| method | similarity
|
|
486
|
-
|
|
487
|
-
| cosine | 1 - distance (
|
|
488
|
-
| l2 | distance (
|
|
489
|
-
| inner_product | -distance (
|
|
485
|
+
| method | similarity meaning | sort order |
|
|
486
|
+
|--------|-------------------|------------|
|
|
487
|
+
| cosine | 1 - distance (higher = more similar) | desc |
|
|
488
|
+
| l2 | distance (lower = more similar) | asc |
|
|
489
|
+
| inner_product | -distance (higher = more similar) | desc |
|
|
490
490
|
|
|
491
491
|
---
|
|
492
492
|
|
|
493
493
|
## pg_trgm Fuzzy Search
|
|
494
494
|
|
|
495
|
-
`CREATE EXTENSION IF NOT EXISTS pg_trgm
|
|
495
|
+
Requires `CREATE EXTENSION IF NOT EXISTS pg_trgm`. Typically used together with a generated column created via the `searchText` prop and a GIN index.
|
|
496
496
|
|
|
497
|
-
### whereFuzzy —
|
|
497
|
+
### whereFuzzy — Candidate Filtering
|
|
498
498
|
|
|
499
|
-
|
|
499
|
+
The SQL operand order differs per operator:
|
|
500
500
|
|
|
501
|
-
| operator |
|
|
502
|
-
|
|
503
|
-
| `<%` (
|
|
501
|
+
| operator | meaning | SQL |
|
|
502
|
+
|----------|---------|-----|
|
|
503
|
+
| `<%` (default) | word similarity | `'query' <% column` |
|
|
504
504
|
| `%` | similarity | `column % 'query'` |
|
|
505
505
|
| `<<%` | strict word similarity | `'query' <<% column` |
|
|
506
506
|
|
|
@@ -510,9 +510,9 @@ puri.whereFuzzy("items.search_text", query, { operator: "%" });
|
|
|
510
510
|
puri.whereFuzzy("items.search_text", query, { operator: "<<%" });
|
|
511
511
|
```
|
|
512
512
|
|
|
513
|
-
###
|
|
513
|
+
### Similarity Scores — Static Methods
|
|
514
514
|
|
|
515
|
-
`SqlExpression<"number"
|
|
515
|
+
Returns `SqlExpression<"number">`, so use it as a score column in select:
|
|
516
516
|
|
|
517
517
|
```typescript
|
|
518
518
|
// Puri.wordSimilarity(column, query) → word_similarity(?, ??)
|
|
@@ -533,13 +533,13 @@ const results = await this.getPuri("r")
|
|
|
533
533
|
.orderByRaw("score DESC");
|
|
534
534
|
```
|
|
535
535
|
|
|
536
|
-
###
|
|
536
|
+
### Language-specific Characteristics
|
|
537
537
|
|
|
538
|
-
|
|
|
539
|
-
|
|
540
|
-
|
|
|
541
|
-
|
|
|
542
|
-
|
|
|
538
|
+
| Language | Suitability | Notes |
|
|
539
|
+
|----------|-------------|-------|
|
|
540
|
+
| English | Excellent | Word-level splitting + word_similarity |
|
|
541
|
+
| Korean | Good | Performance degrades for 1-2 character searches |
|
|
542
|
+
| Japanese | Good | Performance degrades for 1-2 character searches |
|
|
543
543
|
|
|
544
544
|
---
|
|
545
545
|
|
|
@@ -548,4 +548,4 @@ const results = await this.getPuri("r")
|
|
|
548
548
|
- MUST use `getPuri("r")` for read queries, `getPuri("w")` for write queries
|
|
549
549
|
- MUST include WHERE condition for UPDATE/DELETE operations
|
|
550
550
|
- MUST use `transaction()` for multiple write operations
|
|
551
|
-
- JSON/JSONB
|
|
551
|
+
- JSON/JSONB columns are automatically JSON.stringify'd on insert/update
|