This commit is contained in:
2026-06-24 15:57:01 +02:00
commit cc568081d2
19 changed files with 3212 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
.env
node_modules
tests
Dockerfile
src/utils/fetch.ts
fetching.png
migrating.png
+3
View File
@@ -0,0 +1,3 @@
API_PORT=
WEAVIATE_URL=
FRONTEND_ORIGIN=
+6
View File
@@ -0,0 +1,6 @@
.env
node_modules
memes/*
fetching.png
migrating.png
src/utils/fetch.ts
+5
View File
@@ -0,0 +1,5 @@
FROM node:24.7.0-trixie-slim
WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD npm run migrate && npm run start
+1
View File
@@ -0,0 +1 @@
# Backend for MemeSearch 🔎
+32
View File
@@ -0,0 +1,32 @@
---
services:
weaviate:
command:
- --host
- 0.0.0.0
- --port
- '8080'
- --scheme
- http
image: cr.weaviate.io/semitechnologies/weaviate:1.26.1
ports:
- 8080:8080
- 50051:50051
volumes:
- weaviate_data:/var/lib/weaviate
restart: on-failure:0
environment:
CLIP_INFERENCE_API: 'http://multi2vec-clip:8080'
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
DEFAULT_VECTORIZER_MODULE: 'multi2vec-clip'
ENABLE_MODULES: 'multi2vec-clip'
CLUSTER_HOSTNAME: 'node1'
multi2vec-clip:
image: cr.weaviate.io/semitechnologies/multi2vec-clip:sentence-transformers-clip-ViT-B-32-multilingual-v1
environment:
ENABLE_CUDA: '0'
volumes:
weaviate_data:
...
Vendored
+11
View File
@@ -0,0 +1,11 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
API_PORT: string;
WEAVIATE_URL: string;
FRONTEND_ORIGIN: string;
}
}
}
export {}
+2925
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "memesearch-backend",
"type": "module",
"scripts": {
"dev": "tsx watch --env-file=.env --require reflect-metadata --require tsconfig-paths/register --require dotenv-safe/config src/index.ts",
"start": "tsx --env-file=.env --require reflect-metadata --require tsconfig-paths/register --require dotenv-safe/config src/index.ts",
"env:generate": "gen-env-types .env -o env.d.ts -e .",
"fetch": "tsx src/utils/fetch.ts",
"migrate": "tsx src/utils/migrate.ts"
},
"dependencies": {
"@apollo/server": "^4.11.0",
"cheerio": "^1.0.0",
"cors": "^2.8.5",
"dotenv-safe": "^9.1.0",
"express": "^4.19.2",
"graphql": "^16.9.0",
"http": "^0.0.1-security",
"protobufjs": "^7.3.2",
"reflect-metadata": "^0.2.2",
"tsconfig-paths": "^4.2.0",
"type-graphql": "^2.0.0-rc.2",
"weaviate-client": "^3.1.4"
},
"devDependencies": {
"@types/cheerio": "^0.22.35",
"@types/cors": "^2.8.17",
"@types/dotenv-safe": "^8.1.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"gen-env-types": "^1.3.4",
"tsx": "^4.17.0",
"typescript": "^5.5.3"
}
}
+1
View File
@@ -0,0 +1 @@
export const __prod__ = process.env.NODE_ENV ==='production'
+67
View File
@@ -0,0 +1,67 @@
import { __prod__ } from '@/src/constants'
import { MemeResolver } from '@/src/resolvers'
import { Context } from '@/src/types'
import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
import cors from 'cors'
import express from 'express'
import http from 'http'
import { buildSchema } from 'type-graphql'
import weaviate, { WeaviateClient } from 'weaviate-client'
const weaviateClient: WeaviateClient = await weaviate.connectToLocal()
const app = express()
app.options('*', cors({
credentials: true,
origin: process.env.FRONTEND_ORIGIN,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}))
app.use(cors({
credentials: true,
origin: process.env.FRONTEND_ORIGIN,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}))
const httpServer = http.createServer(app)
const apolloServer = new ApolloServer<Context>({
schema: await buildSchema({
resolvers: [
MemeResolver
]
}),
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
})
await apolloServer.start()
app.use(
'/graphql',
cors<cors.CorsRequest>({
origin: process.env.FRONTEND_ORIGIN, // This will ensures that only the client at the FRONTEND_ORIGIN can make requests to the /graphql endpoint.
credentials: true, // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}),
express.json({ limit: '1GB' }),
expressMiddleware(apolloServer, {
context: async ({ req, res }): Promise<Context> => ({ req, res, weaviate: weaviateClient })
})
)
app.listen(parseInt(process.env.API_PORT), () => {
if (!__prod__) {
console.log(`Server started on localhost:${process.env.API_PORT}.`)
}
else {
console.log(`Server started at ${process.env.BACKEND_ORIGIN}.`)
}
})
app.get('/', (_, res) => {
res.send('Start querying at /graphql.')
})
+1
View File
@@ -0,0 +1 @@
export * from './meme'
+31
View File
@@ -0,0 +1,31 @@
import { Context } from '@/src/types'
import { Arg, Ctx, Field, Int, ObjectType, Query, Resolver } from 'type-graphql'
@ObjectType()
class Meme {
@Field(() => String)
text: string
@Field(() => String)
image: string
}
@Resolver(Meme)
export class MemeResolver {
@Query(() => [Meme])
async search(
@Arg('image', () => String, { nullable: true }) image: string,
@Arg('limit', () => Int, { nullable: true }) limit: number = 1,
@Ctx() { weaviate }: Context
): Promise<Meme[]> {
// console.log(await weaviate.collections.listAll())
const memeCollection = weaviate.collections.get('Meme')
const searchFileBuffer = Buffer.from(image, 'base64')
const response = await memeCollection.query.nearImage(searchFileBuffer, {
limit,
returnProperties: ['image', 'text']
})
console.log(response.objects.map(o => ({ image: o.properties.image as string, text: o.properties.text as string })))
return response.objects.map(o => ({ image: o.properties.image as string, text: o.properties.text as string }))
}
}
+8
View File
@@ -0,0 +1,8 @@
import { Request, Response } from 'express'
import { WeaviateClient } from 'weaviate-client'
export interface Context {
req: Request
res: Response
weaviate: WeaviateClient
}
+38
View File
@@ -0,0 +1,38 @@
import * as fs from 'fs'
import * as path from 'path'
import { fileURLToPath } from 'url'
import weaviate, { dataType, vectorizer, WeaviateClient } from 'weaviate-client'
const client: WeaviateClient = await weaviate.connectToLocal()
const collections = await client.collections.listAll()
if (!collections.find(c => c.name == 'Meme')) {
await client.collections.create({
name: 'Meme',
vectorizers: vectorizer.multi2VecClip({
imageFields: ['image'],
textFields: ['text'],
}),
properties: [
{
name: 'image',
dataType: dataType.BLOB
},
{
name: 'text',
dataType: dataType.TEXT
}
]
})
}
const memeCollection = client.collections.get('Meme')
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const memesDir = path.join(__dirname, '../../memes')
const imgFiles = fs.readdirSync(memesDir)
for (let i = 0;i < imgFiles.length;i++) {
const contentsBase64 = await fs.promises.readFile(`${memesDir}/${imgFiles[i]}`, { encoding: 'base64' })
console.log(await memeCollection.data.insert({ image: contentsBase64, text: imgFiles[i] }))
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+41
View File
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"lib": [
"DOM",
"ES2023",
"ESNext"
],
"module": "ESNext",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"paths": {
"@/*": [
"./*"
]
},
"removeComments": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"target": "ES2023"
},
"exclude": [
"node_modules"
],
"include": [
"env.d.ts",
"./src/**/*.ts"
]
}