Add meilisearch

This commit is contained in:
2023-10-27 15:05:18 +02:00
parent e733c6acc8
commit cc4b69ca50
14 changed files with 160 additions and 12229 deletions

View File

@@ -19,4 +19,8 @@ IGNORE_MAILS=true
API_KEYS=SCOROTEST,ROBOTO,SCORO API_KEYS=SCOROTEST,ROBOTO,SCORO
API_KEY_SCORO_TEST=SCOROTEST API_KEY_SCORO_TEST=SCOROTEST
API_KEY_ROBOT=ROBOTO API_KEY_ROBOT=ROBOTO
API_KEY_SCORO=SCORO API_KEY_SCORO=SCORO
MEILI_HTTP_ADDR="http://meilisearch:7700"
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
# vi: ft=sh

12203
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,7 @@
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"json-logger-service": "^9.0.1", "json-logger-service": "^9.0.1",
"meilisearch": "^0.35.0",
"node-fetch": "^2.6.12", "node-fetch": "^2.6.12",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
"opensheetmusicdisplay": "^1.8.4", "opensheetmusicdisplay": "^1.8.4",

View File

@@ -2,9 +2,10 @@ import { Module } from "@nestjs/common";
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from "src/prisma/prisma.module";
import { ArtistController } from "./artist.controller"; import { ArtistController } from "./artist.controller";
import { ArtistService } from "./artist.service"; import { ArtistService } from "./artist.service";
import { SearchModule } from 'src/search/search.module';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule, SearchModule],
controllers: [ArtistController], controllers: [ArtistController],
providers: [ArtistService], providers: [ArtistService],
}) })

View File

@@ -1,15 +1,21 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, Artist } from "@prisma/client"; import { Prisma, Artist } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
import { MeiliService } from 'src/search/meilisearch.service';
@Injectable() @Injectable()
export class ArtistService { export class ArtistService {
constructor(private prisma: PrismaService) {} constructor(
private prisma: PrismaService,
private search: MeiliService,
) {}
async create(data: Prisma.ArtistCreateInput): Promise<Artist> { async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
return this.prisma.artist.create({ const ret = await this.prisma.artist.create({
data, data,
}); });
await this.search.index('artists').addDocuments([ret]);
return ret;
} }
async get( async get(
@@ -42,8 +48,10 @@ export class ArtistService {
} }
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> { async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
return this.prisma.artist.delete({ const ret = await this.prisma.artist.delete({
where, where,
}); });
await this.search.index('artists').deleteDocument(ret.id);
return ret
} }
} }

View File

@@ -0,0 +1,29 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import MeiliSearch, { DocumentOptions, Settings } from 'meilisearch';
@Injectable()
export class MeiliService extends MeiliSearch implements OnModuleInit {
constructor() {
super({
host: process.env.MEILI_ADDR || 'http://meilisearch:7700',
apiKey: process.env.MEILI_MASTER_KEY,
});
}
async definedIndex(uid: string, opts: Settings) {
let task = await this.createIndex(uid, { primaryKey: 'id' });
await this.waitForTask(task.taskUid);
task = await this.index(uid).updateSettings(opts);
await this.waitForTask(task.taskUid);
}
async onModuleInit() {
await this.definedIndex('songs', {
searchableAttributes: ['name', 'artist'],
filterableAttributes: ['artistId', 'genreId'],
});
await this.definedIndex('artists', {
searchableAttributes: ['name'],
});
}
}

View File

@@ -25,27 +25,23 @@ import { SongController } from "src/song/song.controller";
import { GenreController } from "src/genre/genre.controller"; import { GenreController } from "src/genre/genre.controller";
import { ArtistController } from "src/artist/artist.controller"; import { ArtistController } from "src/artist/artist.controller";
@ApiTags("search") @ApiTags('search')
@Controller("search") @Controller('search')
@UseGuards(JwtAuthGuard)
export class SearchController { export class SearchController {
constructor(private readonly searchService: SearchService) {} constructor(private readonly searchService: SearchService) {}
@Get("songs/:query") @Get("songs/:query")
@ApiOkResponse({ type: _Song, isArray: true }) @ApiOkResponse({ type: _Song, isArray: true })
@ApiOperation({ description: "Search a song" }) @ApiOperation({ description: 'Search a song' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@UseGuards(JwtAuthGuard)
async searchSong( async searchSong(
@Request() req: any, @Request() req: any,
@Query("include") include: string, @Param('query') query: string,
@Param("query") query: string, @Param('artistId') artistId: number,
): Promise<Song[] | null> { ): Promise<Song[] | null> {
try { try {
const ret = await this.searchService.songByGuess( const ret = await this.searchService.searchSong(query, artistId);
query,
req.user?.id,
mapInclude(include, req, SongController.includableFields),
);
if (!ret.length) throw new NotFoundException(); if (!ret.length) throw new NotFoundException();
else return ret; else return ret;
} catch (error) { } catch (error) {

View File

@@ -4,11 +4,12 @@ import { SearchController } from "./search.controller";
import { HistoryModule } from "src/history/history.module"; import { HistoryModule } from "src/history/history.module";
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from "src/prisma/prisma.module";
import { SongService } from "src/song/song.service"; import { SongService } from "src/song/song.service";
import { MeiliService } from "./meilisearch.service";
@Module({ @Module({
imports: [PrismaModule, HistoryModule], imports: [PrismaModule, HistoryModule],
controllers: [SearchController], controllers: [SearchController],
providers: [SearchService, SongService], providers: [SearchService, SongService, MeiliService],
exports: [SearchService], exports: [SearchService, MeiliService],
}) })
export class SearchModule {} export class SearchModule {}

View File

@@ -1,26 +1,28 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Artist, Prisma, Song, Genre } from "@prisma/client"; import { Artist, Prisma, Song, Genre } from '@prisma/client';
import { HistoryService } from "src/history/history.service"; import { HistoryService } from 'src/history/history.service';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
import { MeiliService } from './meilisearch.service';
@Injectable() @Injectable()
export class SearchService { export class SearchService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private history: HistoryService, private history: HistoryService,
private search: MeiliService,
) {} ) {}
async songByGuess( async searchSong(query: string, artistId?: number): Promise<Song[]> {
query: string, if (query.length === 0) {
userID: number, return await this.prisma.song.findMany({
include?: Prisma.SongInclude, where: {
): Promise<Song[]> { artistId,
return this.prisma.song.findMany({ },
where: { });
name: { contains: query, mode: "insensitive" }, }
}, return (await this.search
include, .index('songs')
}); .search(query, { filter: `artistId = ${artistId}` })) as any;
} }
async genreByGuess( async genreByGuess(

View File

@@ -1,11 +1,12 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { SongService } from "./song.service"; import { SongService } from './song.service';
import { SongController } from "./song.controller"; import { SongController } from './song.controller';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { HistoryModule } from "src/history/history.module"; import { HistoryModule } from 'src/history/history.module';
import { SearchModule } from 'src/search/search.module';
@Module({ @Module({
imports: [PrismaModule, HistoryModule], imports: [PrismaModule, HistoryModule, SearchModule],
providers: [SongService], providers: [SongService],
controllers: [SongController], controllers: [SongController],
}) })

View File

@@ -1,13 +1,17 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma, Song } from "@prisma/client"; import { Prisma, Song } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { MeiliService } from "src/search/meilisearch.service";
import { generateSongAssets } from "src/assetsgenerator/generateImages_browserless"; import { generateSongAssets } from "src/assetsgenerator/generateImages_browserless";
@Injectable() @Injectable()
export class SongService { export class SongService {
// number is the song id // number is the song id
private assetCreationTasks: Map<number, Promise<void>>; private assetCreationTasks: Map<number, Promise<void>>;
constructor(private prisma: PrismaService) { constructor(
private prisma: PrismaService,
private search: MeiliService,
) {
this.assetCreationTasks = new Map(); this.assetCreationTasks = new Map();
} }
@@ -34,9 +38,19 @@ export class SongService {
} }
async createSong(data: Prisma.SongCreateInput): Promise<Song> { async createSong(data: Prisma.SongCreateInput): Promise<Song> {
return this.prisma.song.create({ const song = await this.prisma.song.create({
data, data,
}); });
// Inculde the name of the artist in the song document to make search easier.
const artist = song.artistId
? await this.prisma.artist.findFirst({
where: { id: song.artistId },
})
: null;
await this.search
.index("songs")
.addDocuments([{ ...song, artist: artist?.name }]);
return song;
} }
async song( async song(
@@ -69,8 +83,10 @@ export class SongService {
} }
async deleteSong(where: Prisma.SongWhereUniqueInput): Promise<Song> { async deleteSong(where: Prisma.SongWhereUniqueInput): Promise<Song> {
return this.prisma.song.delete({ const ret = await this.prisma.song.delete({
where, where,
}); });
await this.search.index("songs").deleteDocument(ret.id);
return ret;
} }
} }

View File

@@ -3,6 +3,7 @@ networks:
volumes: volumes:
scoro_logs: scoro_logs:
meilisearch:
services: services:
@@ -86,3 +87,12 @@ services:
- "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro" - "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro"
ports: ports:
- "4567:4567" - "4567:4567"
meilisearch:
image: getmeili/meilisearch:v1.4
ports:
- "7000:7000"
volumes:
- meilisearch:/meili_data
env_file:
- .env

View File

@@ -3,6 +3,8 @@ networks:
volumes: volumes:
scoro_logs: scoro_logs:
meilisearch:
db:
services: services:
back: back:
@@ -52,4 +54,13 @@ services:
depends_on: depends_on:
- "back" - "back"
env_file: env_file:
- .env - .env
meilisearch:
image: getmeili/meilisearch:v1.4
ports:
- "7000:7000"
volumes:
- meilisearch:/meili_data
env_file:
- .env

View File

@@ -5,6 +5,7 @@ networks:
volumes: volumes:
db: db:
scoro_logs: scoro_logs:
meilisearch:
services: services:
@@ -58,4 +59,13 @@ services:
depends_on: depends_on:
- "back" - "back"
env_file: env_file:
- .env - .env
meilisearch:
image: getmeili/meilisearch:v1.4
ports:
- "7000:7000"
volumes:
- meilisearch:/meili_data
env_file:
- .env