Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lio-p committed Nov 19, 2024
1 parent 441b143 commit 5fa197e
Show file tree
Hide file tree
Showing 31 changed files with 3,797 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

py-embed/.venv
py-embed/__pycache__
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
# ch-image-search-demo
This repository contains an example React NextJS application that leverage Clickhouse vector Search capability to implement image similarity search.

## Prerequisite
- Clickhouse server
- NodeJS LTS
- Python 3.10+

## Architecture

This application has two parts:
1. Python embedding model
2. ReactJS frontend

## Python embedding model

The Python application is used to generate embeddings for the images.

### Setup

Install the dependencies:

```bash
cd py-embed
pip install -r requirements.txt
```

Run the application:

```bash
uvicorn app:app --reload
```

## ReactJS application

The ReactJS application is used to stream images from Clickhouse and also perform image similarity search.

### Setup

Install the dependencies:

```bash
yarn install
```

Run the application:

```bash
yarn dev
```

Build the application:

```bash
yarn build
```
3 changes: 3 additions & 0 deletions app/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
40 changes: 40 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
48 changes: 48 additions & 0 deletions app/api/images/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import clickhouse from '@/utils/clickhouse'

export async function POST(request: Request) {
try {
const { vectorData } = await request.json()
const { searchParams } = new URL(request.url)
const timestampMin = searchParams.get('timestamp_min')
const timestampMax = searchParams.get('timestamp_max')

// Convert the vector data to a string format that ClickHouse can understand
const vectorString = `[${vectorData.join(',')}]`
if (timestampMin != null && timestampMax != null) {
const result = await clickhouse.query({
query: `
SELECT id, base64_data, L2Distance(image_embedding, ${vectorString}) as score
FROM social_posts_with_images WHERE similarity >= 0.2 and timestamp > parseDateTimeBestEffort({timestampMin:String}) and timestamp < parseDateTimeBestEffort({timestampMax:String})
ORDER BY score ASC
LIMIT 4
SETTINGS enable_analyzer = 1
`,
format: 'JSONEachRow',
query_params: {
timestampMin: timestampMin,
timestampMax: timestampMax
}
})
const data = await result.json()
return NextResponse.json(data)
} else {
const result = await clickhouse.query({
query: `
SELECT id, base64_data, L2Distance(image_embedding, ${vectorString}) as score
FROM social_posts_with_images WHERE similarity >= 0.2
ORDER BY score ASC
LIMIT 4
SETTINGS enable_analyzer = 0
`,
format: 'JSONEachRow',
})
const data = await result.json()
return NextResponse.json(data)
}
} catch (error) {
console.error('Search error:', error)
return NextResponse.json({ error: 'Failed to search images' }, { status: 500 })
}
}
40 changes: 40 additions & 0 deletions app/api/images/stream/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server'
import clickhouse from '@/utils/clickhouse'

interface ImageRow {
url: string;
timestamp: string;
id: string;
base64_data: string;
}

export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const defaultTimestamp = new Date('2024-11-15').toISOString().replace('T', ' ').split('.')[0]
const timestamp = searchParams.get('timestamp') || defaultTimestamp
try {
const result = await clickhouse.query({
query: `
SELECT url, timestamp, id, base64_data
FROM social_posts_with_images
WHERE timestamp > {timestamp:String} and height > 100 and width > 200
ORDER BY timestamp ASC
LIMIT 10
`,
format: 'JSONEachRow',
query_params: {
timestamp: timestamp
}
})

const data = await result.json()
const response: ImageRow[] = []
for (const row of data as ImageRow[] ) {
response.push({ ...row })
}
return NextResponse.json(response)
} catch (error) {
console.error(error)
return NextResponse.json({ error: 'Failed to fetch images' }, { status: 500 })
}
}
Binary file added app/favicon.ico
Binary file not shown.
Binary file added app/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file added app/fonts/GeistVF.woff
Binary file not shown.
27 changes: 27 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--ch-yellow: #FFFF76;
--ch-dark: #1A1A1A;
--ch-darker: #141414;
}

@layer components {
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
}

html, body { overflow: hidden; }


body {
background-color: var(--ch-dark);
color: white;
}
17 changes: 17 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import './globals.css'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<main className="container mx-auto p-4">
{children}
</main>
</body>
</html>
)
}
18 changes: 18 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

import Main from '@/components/Main'

export default function Home() {



return (
<div className="min-h-screen bg-[#1A1A1A] overflow-hidden">
<header className="bg-[#141414] p-4 border-b border-gray-800">
<h1 className="text-2xl font-bold text-white">Real time vector search</h1>
</header>


<Main />
</div>
)
}
Loading

0 comments on commit 5fa197e

Please sign in to comment.