---
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