Add meilisearch
This commit is contained in:
@@ -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
12203
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
back/src/search/meilisearch.service.ts
Normal file
29
back/src/search/meilisearch.service.ts
Normal 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'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user