Skip to content

Commit 581ce43

Browse files
authored
add: guide with generated columns and full text search (#491)
1 parent 41d169b commit 581ce43

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

src/content/docs/guides/_map.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121
["postgresql-local-setup", "Local setup of PostgreSQL"],
2222
["mysql-local-setup", "Local setup of MySQL"],
2323
["seeding-with-partially-exposed-tables", "Seeding Partially Exposed Tables with Foreign Key"],
24-
["seeding-using-with-option", "Seeding using 'with' option"]
24+
["seeding-using-with-option", "Seeding using 'with' option"],
25+
["full-text-search-with-generated-columns", "Full-text search with Generated Columns"]
2526
]
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: Full-text search with Generated Columns
3+
slug: full-text-search-with-generated-columns
4+
---
5+
6+
import Section from "@mdx/Section.astro";
7+
import Prerequisites from "@mdx/Prerequisites.astro";
8+
import CodeTabs from '@mdx/CodeTabs.astro';
9+
import CodeTab from '@mdx/CodeTab.astro';
10+
11+
<Prerequisites>
12+
- Get started with [PostgreSQL](/docs/get-started-postgresql)
13+
- [Select statement](/docs/select)
14+
- [Indexes](/docs/indexes-constraints#indexes)
15+
- [sql operator](/docs/sql)
16+
- [Full-text search](/learn/guides/postgresql-full-text-search)
17+
- [Generated columns](/docs/generated-columns)
18+
</Prerequisites>
19+
20+
This guide demonstrates how to implement full-text search in PostgreSQL with Drizzle and generated columns. A generated column is a special column that is always computed from other columns. It is useful because you don't have to compute the value of the column every time you query the table:
21+
22+
<CodeTabs items={["schema.ts", "migration.sql"]}>
23+
<CodeTab>
24+
```ts copy {18,19,20,23}
25+
import { SQL, sql } from 'drizzle-orm';
26+
import { index, pgTable, serial, text, customType } from 'drizzle-orm/pg-core';
27+
28+
export const tsvector = customType<{
29+
data: string;
30+
}>({
31+
dataType() {
32+
return `tsvector`;
33+
},
34+
});
35+
36+
export const posts = pgTable(
37+
'posts',
38+
{
39+
id: serial('id').primaryKey(),
40+
title: text('title').notNull(),
41+
body: text('body').notNull(),
42+
bodySearch: tsvector('body_search')
43+
.notNull()
44+
.generatedAlwaysAs((): SQL => sql`to_tsvector('english', ${posts.body})`),
45+
},
46+
(t) => [
47+
index('idx_body_search').using('gin', t.bodySearch),
48+
]
49+
);
50+
```
51+
</CodeTab>
52+
```sql
53+
CREATE TABLE "posts" (
54+
"id" serial PRIMARY KEY NOT NULL,
55+
"title" text NOT NULL,
56+
"body" text NOT NULL,
57+
"body_search" "tsvector" GENERATED ALWAYS AS (to_tsvector('english', "posts"."body")) STORED NOT NULL
58+
);
59+
--> statement-breakpoint
60+
CREATE INDEX "idx_body_search" ON "posts" USING gin ("body_search");
61+
```
62+
</CodeTabs>
63+
64+
When you insert a row into a table, the value of a generated column is computed from an expression that you provide when you create the column:
65+
66+
<Section>
67+
```ts
68+
import { posts } from './schema';
69+
70+
const db = drizzle(...);
71+
72+
const body = "Golden leaves cover the quiet streets as a crisp breeze fills the air, bringing the scent of rain and the promise of change"
73+
74+
await db.insert(posts).values({
75+
body,
76+
title: "The Beauty of Autumn",
77+
}
78+
).returning();
79+
```
80+
81+
```json
82+
[
83+
{
84+
id: 1,
85+
title: 'The Beauty of Autumn',
86+
body: 'Golden leaves cover the quiet streets as a crisp breeze fills the air, bringing the scent of rain and the promise of change',
87+
bodySearch: "'air':13 'breez':10 'bring':14 'chang':23 'cover':3 'crisp':9 'fill':11 'golden':1 'leav':2 'promis':21 'quiet':5 'rain':18 'scent':16 'street':6"
88+
}
89+
]
90+
```
91+
</Section>
92+
93+
This is how you can implement full-text search with generated columns in PostgreSQL with Drizzle ORM. The `@@` operator is used for direct matches:
94+
95+
<Section>
96+
```ts copy {6}
97+
const searchParam = "bring";
98+
99+
await db
100+
.select()
101+
.from(posts)
102+
.where(sql`${posts.bodySearch} @@ to_tsquery('english', ${searchParam})`);
103+
```
104+
105+
```sql
106+
select * from posts where body_search @@ to_tsquery('english', 'bring');
107+
```
108+
</Section>
109+
110+
This is more advanced schema with a generated column. The `search` column is generated from the `title` and `body` columns and `setweight()` function is used to assign different weights to the columns for full-text search.
111+
This is typically used to mark entries coming from different parts of a document, such as title versus body.
112+
113+
<CodeTabs items={["schema.ts", "migration.sql"]}>
114+
<CodeTab>
115+
```ts copy {18,19,20,21,22,23,24,28}
116+
import { SQL, sql } from 'drizzle-orm';
117+
import { index, pgTable, serial, text, customType } from 'drizzle-orm/pg-core';
118+
119+
export const tsvector = customType<{
120+
data: string;
121+
}>({
122+
dataType() {
123+
return `tsvector`;
124+
},
125+
});
126+
127+
export const posts = pgTable(
128+
'posts',
129+
{
130+
id: serial('id').primaryKey(),
131+
title: text('title').notNull(),
132+
body: text('body').notNull(),
133+
search: tsvector('search')
134+
.notNull()
135+
.generatedAlwaysAs(
136+
(): SQL =>
137+
sql`setweight(to_tsvector('english', ${posts.title}), 'A')
138+
||
139+
setweight(to_tsvector('english', ${posts.body}), 'B')`,
140+
),
141+
},
142+
(t) => [
143+
index('idx_search').using('gin', t.search),
144+
],
145+
);
146+
```
147+
</CodeTab>
148+
```sql
149+
CREATE TABLE "posts" (
150+
"id" serial PRIMARY KEY NOT NULL,
151+
"title" text NOT NULL,
152+
"body" text NOT NULL,
153+
"search" "tsvector" GENERATED ALWAYS AS (setweight(to_tsvector('english', "posts"."title"), 'A')
154+
||
155+
setweight(to_tsvector('english', "posts"."body"), 'B')) STORED NOT NULL
156+
);
157+
--> statement-breakpoint
158+
CREATE INDEX "idx_search" ON "posts" USING gin ("search");
159+
```
160+
</CodeTabs>
161+
162+
This is how you can query the table with full-text search:
163+
164+
<Section>
165+
```ts copy {6}
166+
const search = 'travel';
167+
168+
await db
169+
.select()
170+
.from(posts)
171+
.where(sql`${posts.search} @@ to_tsquery('english', ${search})`);
172+
```
173+
174+
```sql
175+
select * from posts where search @@ to_tsquery('english', 'travel');
176+
```
177+
</Section>

0 commit comments

Comments
 (0)