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:
parent
5f62cefe31
commit
5c08f2b93b
18 changed files with 257 additions and 91 deletions
|
@ -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 └───────────────────────────────────────────
|
||||||
|
|
|
@ -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 └───────────────────────────────────────────
|
||||||
|
|
|
@ -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 └───────────────────────────────────────────
|
||||||
|
|
|
@ -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
2
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
- Node.js 18.6.0以上が必要になりました
|
- Node.js 18.6.0以上が必要になりました
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
- Meilisearchを全文検索に使用できるようになりました
|
||||||
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
||||||
- ユーザーへの自分用メモ機能
|
- ユーザーへの自分用メモ機能
|
||||||
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
||||||
|
|
|
@ -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 └───────────────────────────────────────────
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
166
packages/backend/src/core/SearchService.ts
Normal file
166
packages/backend/src/core/SearchService.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue