feat: Introduce Meilisearch (#10755)

* wip

* wip

* Update SearchService.ts

* Update SearchService.ts

* wip

* wip

* Update SearchService.ts

* Update CHANGELOG.md

* wip

* Update SearchService.ts

* Update docker-compose.yml.example
This commit is contained in:
syuilo 2023-05-05 08:52:14 +09:00 committed by GitHub
parent 5f62cefe31
commit 5c08f2b93b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 257 additions and 91 deletions

View file

@ -95,15 +95,13 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# ┌───────────────────────────── # ┌───────────────────────────
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
#elasticsearch: #meilisearch:
# host: localhost # host: meilisearch
# port: 9200 # port: 7700
# ssl: false # apiKey: ''
# user:
# pass:
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View file

@ -95,15 +95,13 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# ┌───────────────────────────── # ┌───────────────────────────
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
#elasticsearch: #meilisearch:
# host: localhost # host: localhost
# port: 9200 # port: 7700
# ssl: false # apiKey: ''
# user:
# pass:
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View file

@ -95,15 +95,13 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# ┌───────────────────────────── # ┌───────────────────────────
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
#elasticsearch: #meilisearch:
# host: localhost # host: meilisearch
# port: 9200 # port: 7700
# ssl: false # apiKey: ''
# user:
# pass:
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View file

@ -8,7 +8,6 @@ build/
built/ built/
db/ db/
docker-compose.yml docker-compose.yml
elasticsearch/
node_modules/ node_modules/
packages/*/node_modules packages/*/node_modules
redis/ redis/

2
.gitignore vendored
View file

@ -44,7 +44,7 @@ built
/data /data
/.cache-loader /.cache-loader
/db /db
/elasticsearch /meili_data
npm-debug.log npm-debug.log
*.pem *.pem
run.bat run.bat

View file

@ -17,6 +17,7 @@
- Node.js 18.6.0以上が必要になりました - Node.js 18.6.0以上が必要になりました
### General ### General
- Meilisearchを全文検索に使用できるようになりました
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加 - 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能 - ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。

View file

@ -116,15 +116,13 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
# ┌───────────────────────────── # ┌───────────────────────────
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────
#elasticsearch: #meilisearch:
# host: localhost # host: localhost
# port: 9200 # port: 7700
# ssl: false # apiKey: ''
# user:
# pass:
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View file

@ -7,7 +7,7 @@ services:
links: links:
- db - db
- redis - redis
# - es # - meilisearch
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -48,16 +48,18 @@ services:
interval: 5s interval: 5s
retries: 20 retries: 20
# es: # meilisearch:
# restart: always # restart: always
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2 # image: getmeili/meilisearch:v1.1.1
# environment: # environment:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # - MEILI_NO_ANALYTICS=true
# - "TAKE_FILE_OWNERSHIP=111" # - MEILI_ENV=production
# env_file:
# - .config/meilisearch.env
# networks: # networks:
# - internal_network # - internal_network
# volumes: # volumes:
# - ./elasticsearch:/usr/share/elasticsearch/data # - ./meili_data:/meili_data
networks: networks:
internal_network: internal_network:

View file

@ -91,6 +91,7 @@
"jsdom": "21.1.1", "jsdom": "21.1.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.1.1", "jsonld": "8.1.1",
"meilisearch": "0.32.3",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",

View file

@ -2,6 +2,7 @@ import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common'; import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { MeiliSearch } from 'meilisearch';
import { DI } from './di-symbols.js'; import { DI } from './di-symbols.js';
import { loadConfig } from './config.js'; import { loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js'; import { createPostgresDataSource } from './postgres.js';
@ -22,6 +23,21 @@ const $db: Provider = {
inject: [DI.config], inject: [DI.config],
}; };
const $meilisearch: Provider = {
provide: DI.meilisearch,
useFactory: (config) => {
if (config.meilisearch) {
return new MeiliSearch({
host: `http://${config.meilisearch.host}:${config.meilisearch.port}`,
apiKey: config.meilisearch.apiKey,
});
} else {
return null;
}
},
inject: [DI.config],
};
const $redis: Provider = { const $redis: Provider = {
provide: DI.redis, provide: DI.redis,
useFactory: (config) => { useFactory: (config) => {
@ -73,8 +89,8 @@ const $redisForSub: Provider = {
@Global() @Global()
@Module({ @Module({
imports: [RepositoryModule], imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisForPub, $redisForSub], providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule], exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
}) })
export class GlobalModule implements OnApplicationShutdown { export class GlobalModule implements OnApplicationShutdown {
constructor( constructor(

View file

@ -57,13 +57,10 @@ export type Source = {
db?: number; db?: number;
prefix?: string; prefix?: string;
}; };
elasticsearch: { meilisearch?: {
host: string; host: string;
port: number; port: string;
ssl?: boolean; apiKey: string;
user?: string;
pass?: string;
index?: string;
}; };
proxy?: string; proxy?: string;
@ -139,6 +136,7 @@ const path = process.env.MISSKEY_CONFIG_YML
: process.env.NODE_ENV === 'test' : process.env.NODE_ENV === 'test'
? resolve(dir, 'test.yml') ? resolve(dir, 'test.yml')
: resolve(dir, 'default.yml'); : resolve(dir, 'default.yml');
export function loadConfig() { export function loadConfig() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');

View file

@ -50,6 +50,7 @@ import { WebhookService } from './WebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js'; import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js'; import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js'; import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js'; import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js'; import NotesChart from './chart/charts/notes.js';
@ -171,6 +172,8 @@ const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', u
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart }; const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart };
@ -295,6 +298,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookService, WebhookService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
SearchService,
ChartLoggerService, ChartLoggerService,
FederationChart, FederationChart,
NotesChart, NotesChart,
@ -413,6 +417,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookService, $WebhookService,
$UtilityService, $UtilityService,
$FileInfoService, $FileInfoService,
$SearchService,
$ChartLoggerService, $ChartLoggerService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
@ -532,6 +537,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookService, WebhookService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
SearchService,
FederationChart, FederationChart,
NotesChart, NotesChart,
UsersChart, UsersChart,
@ -649,6 +655,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookService, $WebhookService,
$UtilityService, $UtilityService,
$FileInfoService, $FileInfoService,
$SearchService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
$UsersChart, $UsersChart,

View file

@ -46,6 +46,7 @@ import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService, private metaService: MetaService,
private searchService: SearchService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
@ -728,17 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private index(note: Note) { private index(note: Note) {
if (note.text == null || this.config.elasticsearch == null) return; if (note.text == null && note.cw == null) return;
/*
es!.index({ this.searchService.indexNote(note);
index: this.config.elasticsearch.index ?? 'misskey_note',
id: note.id.toString(),
body: {
text: normalizeForSearch(note.text),
userId: note.userId,
userHost: note.userHost,
},
});*/
} }
@bindThis @bindThis

View file

@ -0,0 +1,166 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { Note } from '@/models/entities/Note.js';
import { User } from '@/models/index.js';
import type { NotesRepository } from '@/models/index.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { QueryService } from '@/core/QueryService.js';
import { IdService } from '@/core/IdService.js';
import type { Index, MeiliSearch } from 'meilisearch';
type K = string;
type V = string | number | boolean;
type Q =
{ op: '=', k: K, v: V } |
{ op: '!=', k: K, v: V } |
{ op: '>', k: K, v: number } |
{ op: '<', k: K, v: number } |
{ op: '>=', k: K, v: number } |
{ op: '<=', k: K, v: number } |
{ op: 'and', qs: Q[] } |
{ op: 'or', qs: Q[] } |
{ op: 'not', q: Q };
function compileValue(value: V): string {
if (typeof value === 'string') {
return `'${value}'`; // TODO: escape
} else if (typeof value === 'number') {
return value.toString();
} else if (typeof value === 'boolean') {
return value.toString();
}
throw new Error('unrecognized value');
}
function compileQuery(q: Q): string {
switch (q.op) {
case '=': return `(${q.k} = ${compileValue(q.v)})`;
case '!=': return `(${q.k} != ${compileValue(q.v)})`;
case '>': return `(${q.k} > ${compileValue(q.v)})`;
case '<': return `(${q.k} < ${compileValue(q.v)})`;
case '>=': return `(${q.k} >= ${compileValue(q.v)})`;
case '<=': return `(${q.k} <= ${compileValue(q.v)})`;
case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`;
case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`;
case 'not': return `(NOT ${compileQuery(q.q)})`;
default: throw new Error('unrecognized query operator');
}
}
@Injectable()
export class SearchService {
private meilisearchNoteIndex: Index | null = null;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.meilisearch)
private meilisearch: MeiliSearch | null,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private queryService: QueryService,
private idService: IdService,
) {
if (meilisearch) {
this.meilisearchNoteIndex = meilisearch.index('notes');
this.meilisearchNoteIndex.updateSettings({
searchableAttributes: [
'text',
'cw',
],
sortableAttributes: [
'createdAt',
],
filterableAttributes: [
'createdAt',
'userId',
'userHost',
'channelId',
],
typoTolerance: {
enabled: false,
},
pagination: {
maxTotalHits: 10000,
},
});
}
}
@bindThis
public async indexNote(note: Note): Promise<void> {
if (this.meilisearch) {
this.meilisearchNoteIndex!.addDocuments([{
id: note.id,
createdAt: note.createdAt.getTime(),
userId: note.userId,
userHost: note.userHost,
channelId: note.channelId,
cw: note.cw,
text: note.text,
}], {
primaryKey: 'id',
});
}
}
@bindThis
public async searchNote(q: string, me: User | null, opts: {
userId?: Note['userId'] | null;
channelId?: Note['channelId'] | null;
}, pagination: {
untilId?: Note['id'];
sinceId?: Note['id'];
limit?: number;
}): Promise<Note[]> {
if (this.meilisearch) {
const filter: Q = {
op: 'and',
qs: [],
};
if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() });
if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
const res = await this.meilisearchNoteIndex!.search(q, {
sort: ['createdAt:desc'],
matchingStrategy: 'all',
attributesToRetrieve: ['id', 'createdAt'],
filter: compileQuery(filter),
limit: pagination.limit,
});
if (res.hits.length === 0) return [];
return await this.notesRepository.findBy({
id: In(res.hits.map(x => x.id)),
});
} else {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
if (opts.userId) {
query.andWhere('note.userId = :userId', { userId: opts.userId });
} else if (opts.channelId) {
query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
}
query
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
return await query.take(pagination.limit).getMany();
}
}
}

View file

@ -1,6 +1,7 @@
export const DI = { export const DI = {
config: Symbol('config'), config: Symbol('config'),
db: Symbol('db'), db: Symbol('db'),
meilisearch: Symbol('meilisearch'),
redis: Symbol('redis'), redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'), redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'), redisForSub: Symbol('redisForSub'),

View file

@ -201,10 +201,6 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
elasticsearch: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptcha: { hcaptcha: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -331,7 +327,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
response.features = { response.features = {
registration: !instance.disableRegistration, registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
elasticsearch: this.config.elasticsearch ? true : false,
hcaptcha: instance.enableHcaptcha, hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha, recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile, turnstile: instance.enableTurnstile,

View file

@ -1,11 +1,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/index.js'; import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -61,11 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private searchService: SearchService,
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -74,27 +70,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); const notes = await this.searchService.searchNote(ps.query, me, {
userId: ps.userId,
if (ps.userId) { channelId: ps.channelId,
query.andWhere('note.userId = :userId', { userId: ps.userId }); }, {
} else if (ps.channelId) { untilId: ps.untilId,
query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); sinceId: ps.sinceId,
} limit: ps.limit,
});
query
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
const notes = await query.take(ps.limit).getMany();
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });

View file

@ -229,6 +229,9 @@ importers:
jsrsasign: jsrsasign:
specifier: 10.8.6 specifier: 10.8.6
version: 10.8.6 version: 10.8.6
meilisearch:
specifier: 0.32.3
version: 0.32.3
mfm-js: mfm-js:
specifier: 0.23.3 specifier: 0.23.3
version: 0.23.3 version: 0.23.3
@ -9582,7 +9585,6 @@ packages:
node-fetch: 2.6.7 node-fetch: 2.6.7
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
dev: true
/cross-spawn@5.1.0: /cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
@ -14496,6 +14498,14 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: true dev: true
/meilisearch@0.32.3:
resolution: {integrity: sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw==}
dependencies:
cross-fetch: 3.1.5
transitivePeerDependencies:
- encoding
dev: false
/memoizerific@1.11.3: /memoizerific@1.11.3:
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
dependencies: dependencies:
@ -14657,6 +14667,7 @@ packages:
/minimist@1.2.7: /minimist@1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
dev: false
/minimist@1.2.8: /minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
@ -19700,7 +19711,7 @@ packages:
axios: 0.27.2(debug@4.3.4) axios: 0.27.2(debug@4.3.4)
joi: 17.7.0 joi: 17.7.0
lodash: 4.17.21 lodash: 4.17.21
minimist: 1.2.7 minimist: 1.2.8
rxjs: 7.8.1 rxjs: 7.8.1
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug