---
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
tests
|
||||||
|
Dockerfile
|
||||||
|
src/utils/fetch.ts
|
||||||
|
fetching.png
|
||||||
|
migrating.png
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
API_PORT=
|
||||||
|
WEAVIATE_URL=
|
||||||
|
FRONTEND_ORIGIN=
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
memes/*
|
||||||
|
fetching.png
|
||||||
|
migrating.png
|
||||||
|
src/utils/fetch.ts
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
|
...
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
API_PORT: string;
|
||||||
|
WEAVIATE_URL: string;
|
||||||
|
FRONTEND_ORIGIN: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
Generated
+2925
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const __prod__ = process.env.NODE_ENV ==='production'
|
||||||
@@ -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.')
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './meme'
|
||||||
@@ -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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
import { WeaviateClient } from 'weaviate-client'
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
req: Request
|
||||||
|
res: Response
|
||||||
|
weaviate: WeaviateClient
|
||||||
|
}
|
||||||
@@ -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] }))
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user