Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ This is a monorepo containing multiple standalone projects. Each project lives i

```plaintext
code-samples/
├── typesense-angular-search-bar/ # Angular + Typesense search implementation
├── typesense-astro-search/ # Astro + Typesense search implementation
├── typesense-gin-full-text-search/ # Go (Gin) + Typesense backend implementation
├── typesense-next-search-bar/ # Next.js + Typesense search implementation
├── typesense-nuxt-search-bar/ # Nuxt.js + Typesense search implementation
├── typesense-qwik-js-search/ # Qwik + Typesense search implementation
├── typesense-react-native-search-bar/ # React Native + Typesense search implementation
├── typesense-solid-js-search/ # SolidJS + Typesense search implementation
├── typesense-vanilla-js-search/ # Vanilla JS + Typesense search implementation
└── README.md # You are here
├── typesense-angular-search-bar/ # Angular + Typesense search implementation
├── typesense-astro-search/ # Astro + Typesense search implementation
├── typesense-gin-full-text-search/ # Go (Gin) + Typesense backend implementation
├── typesense-next-search-bar/ # Next.js + Typesense search implementation
├── typesense-nuxt-search-bar/ # Nuxt.js + Typesense search implementation
├── typesense-qwik-js-search/ # Qwik + Typesense search implementation
├── typesense-react-native-search-bar/ # React Native + Typesense search implementation
├── typesense-solid-js-search/ # SolidJS + Typesense search implementation
├── typesense-springboot-full-text-search/ # Spring Boot + Typesense backend implementation
├── typesense-node-prisma-full-text-search/ # Node.js (Express) + Typesense + Prisma backend implementation
├── typesense-node-sequelize-full-text-search/ # Node.js (Express) + Typesense + Sequelize backend implementation
├── typesense-node-drizzle-full-text-search/ # Node.js (Express) + Typesense + Drizzle backend implementation
├── typesense-vanilla-js-search/ # Vanilla JS + Typesense search implementation
└── README.md # You are here
```

## Projects
Expand All @@ -32,6 +36,10 @@ code-samples/
| [typesense-qwik-js-search](./typesense-qwik-js-search) | Qwik | Resumable search bar with real-time search and modern UI |
| [typesense-react-native-search-bar](./typesense-react-native-search-bar) | React Native | A mobile search bar with instant search capabilities |
| [typesense-solid-js-search](./typesense-solid-js-search) | SolidJS | A modern search bar with instant search capabilities |
| [typesense-springboot-full-text-search](./typesense-springboot-full-text-search) | Spring Boot | Backend API with full-text search using Typesense |
| [typesense-node-prisma-full-text-search](./typesense-node-prisma-full-text-search) | Node.js (Express) + Typesense + Prisma | Backend API with full-text search using Typesense |
| [typesense-node-sequelize-full-text-search](./typesense-node-sequelize-full-text-search) | Node.js (Express) + Typesense + Sequelize | Backend API with full-text search using Typesense |
| [typesense-node-drizzle-search-app](./typesense-node-drizzle-search-app) | Node.js (Express) + Typesense + Drizzle | Backend API with full-text search using Typesense |
| [typesense-vanilla-js-search](./typesense-vanilla-js-search) | Vanilla JS | A modern search bar with instant search capabilities |

## Getting Started
Expand Down
54 changes: 54 additions & 0 deletions typesense-node-drizzle-full-text-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Typesense Node.js Drizzle ORM Full-Text Search App

A production-ready RESTful search API built with Node.js, Express, Drizzle ORM, PostgreSQL, and Typesense.

This application maintains PostgreSQL as the primary source of truth while keeping Typesense synchronously and asynchronously updated to handle fast, typo-tolerant full-text searches.

## Features
- **Drizzle ORM Integration**: High performance, strongly-typed PostgreSQL queries.
- **Batched Incremental Sync**: Handles millions of rows without memory bloat using cursor-based pagination.
- **Soft Delete Support**: Properly handles `deleted_at` fields and purges ghosts from Typesense.
- **Cron Jobs**: Background worker keeps the database and Typesense index synchronized automatically.

## Prerequisites
- Node.js v18+
- Docker

## Setup & Running

1. **Start Typesense and PostgreSQL:**
```bash
docker run -d -p 8108:8108 \
-v "$(pwd)"/typesense-data:/data \
typesense/typesense:27.1 \
--data-dir /data \
--api-key=xyz \
--enable-cors

docker run -d \
--name local_postgres \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=admin123 \
-e POSTGRES_DB=testdb \
-p 5432:5432 \
postgres:16
```

2. **Install dependencies:**
```bash
npm install
```

3. **Generate and Run Migrations:**
Generate Drizzle migrations from `src/db/schema.ts` and push them to the database.
```bash
npx drizzle-kit generate
npx drizzle-kit push
```

4. **Start the application:**
```bash
npm run dev
```

The app will connect to PostgreSQL, initialize Typesense schemas, perform a startup sync (if needed), start the cron worker, and bind to `http://localhost:3002`.
11 changes: 11 additions & 0 deletions typesense-node-drizzle-full-text-search/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit';
import 'dotenv/config';

export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE "books" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar(255) NOT NULL,
"authors" json DEFAULT '[]' NOT NULL,
"publication_year" integer,
"average_rating" numeric(3, 2),
"image_url" varchar(255),
"ratings_count" integer,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"deleted_at" timestamp
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"id": "527348cf-95fc-4d6b-bb93-7beac078119f",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.books": {
"name": "books",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"authors": {
"name": "authors",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'[]'"
},
"publication_year": {
"name": "publication_year",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"average_rating": {
"name": "average_rating",
"type": "numeric(3, 2)",
"primaryKey": false,
"notNull": false
},
"image_url": {
"name": "image_url",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"ratings_count": {
"name": "ratings_count",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "CURRENT_TIMESTAMP"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions typesense-node-drizzle-full-text-search/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1777712202767,
"tag": "0000_aromatic_spiral",
"breakpoints": true
}
]
}
34 changes: 34 additions & 0 deletions typesense-node-drizzle-full-text-search/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "typesense-node-drizzle-full-text-search",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"node-cron": "^4.2.1",
"pg": "^8.20.0",
"typesense": "^3.0.6"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^25.6.0",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"ts-node-dev": "^2.0.0",
"typescript": "^6.0.3"
}
}
12 changes: 12 additions & 0 deletions typesense-node-drizzle-full-text-search/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { env } from './env';
import * as schema from '../db/schema';

// Create a pg pool
const pool = new Pool({
connectionString: env.DATABASE_URL,
});

// Create the Drizzle instance
export const db = drizzle(pool, { schema });
27 changes: 27 additions & 0 deletions typesense-node-drizzle-full-text-search/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as dotenv from 'dotenv';
dotenv.config();

const requiredEnvs = [
'DATABASE_URL',
'TYPESENSE_HOST',
'TYPESENSE_PORT',
'TYPESENSE_PROTOCOL',
'TYPESENSE_API_KEY',
'TYPESENSE_COLLECTION',
] as const;

for (const env of requiredEnvs) {
if (!process.env[env]) {
throw new Error(`Missing required environment variable: ${env}`);
}
}

export const env = {
PORT: process.env.PORT || 3000,
DATABASE_URL: process.env.DATABASE_URL!,
TYPESENSE_HOST: process.env.TYPESENSE_HOST!,
TYPESENSE_PORT: parseInt(process.env.TYPESENSE_PORT!, 10),
TYPESENSE_PROTOCOL: process.env.TYPESENSE_PROTOCOL!,
TYPESENSE_API_KEY: process.env.TYPESENSE_API_KEY!,
TYPESENSE_COLLECTION: process.env.TYPESENSE_COLLECTION!,
};
18 changes: 18 additions & 0 deletions typesense-node-drizzle-full-text-search/src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { pgTable, serial, varchar, json, integer, decimal, timestamp } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

export const books = pgTable('books', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
authors: json('authors').default('[]').notNull(),
publicationYear: integer('publication_year'),
averageRating: decimal('average_rating', { precision: 3, scale: 2 }),
imageUrl: varchar('image_url', { length: 255 }),
ratingsCount: integer('ratings_count'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').default(sql`CURRENT_TIMESTAMP`).$onUpdate(() => sql`CURRENT_TIMESTAMP`).notNull(),
deletedAt: timestamp('deleted_at'),
});

export type Book = typeof books.$inferSelect;
export type NewBook = typeof books.$inferInsert;
Loading