Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 検索でMeiliSearchを使えるようにする #10751

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .config/docker_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,41 @@ redis:
# user:
# pass:

# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────

# Refer to https://www.meilisearch.com/docs for more details.
# meilisearch:
# host: meilisearch
# port: 7700
# apiKey: ''
# index: 'misskey'
# searchType: 'all'
# indexLocalOnly: false
# config: {
# rankingRules: [
# 'words',
# 'typo',
# 'sort',
# 'proximity',
# 'attribute',
# 'exactness',
# ],
# searchableAttributes: [
# 'text',
# 'cw',
# ],
# sortableAttributes: [
# 'createdAt',
# ],
# typoTolerance: {
# enabled: false,
# },
# pagination: {
# maxTotalHits: 10000,
# },
# }

# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

Expand Down
35 changes: 35 additions & 0 deletions .config/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,41 @@ redis:
# user:
# pass:

# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────

# Refer to https://www.meilisearch.com/docs for more details.
# meilisearch:
# host: localhost
# port: 7700
# apiKey: ''
# index: 'misskey'
# searchType: 'all'
# indexLocalOnly: false
# config: {
# rankingRules: [
# 'words',
# 'typo',
# 'sort',
# 'proximity',
# 'attribute',
# 'exactness',
# ],
# searchableAttributes: [
# 'text',
# 'cw',
# ],
# sortableAttributes: [
# 'createdAt',
# ],
# typoTolerance: {
# enabled: false,
# },
# pagination: {
# maxTotalHits: 10000,
# },
# }

# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

Expand Down
1 change: 1 addition & 0 deletions .config/meilisearch.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MEILI_MASTER_KEY=yourMasterKey
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
!/.config/meilisearch.env.example
docker-compose.yml
!/.devcontainer/docker-compose.yml

Expand All @@ -45,6 +46,7 @@ built
/.cache-loader
/db
/elasticsearch
/meili_data
npm-debug.log
*.pem
run.bat
Expand Down
14 changes: 14 additions & 0 deletions docker-compose.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
- db
- redis
# - es
# - meilisearch
depends_on:
db:
condition: service_healthy
Expand Down Expand Up @@ -59,6 +60,19 @@ services:
# volumes:
# - ./elasticsearch:/usr/share/elasticsearch/data

meilisearch:
restart: always
image: getmeili/meilisearch:v1.1.1
environment:
- MEILI_NO_ANALYTICS=true
- MEILI_ENV=production
env_file:
- .config/meilisearch.env
networks:
- internal_network
volumes:
- ./meili_data:/meili_data

networks:
internal_network:
internal: true
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsrsasign": "10.8.2",
"meilisearch": "^0.32.3",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/GlobalModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { MeiliService } from './MeiliService.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
Expand Down Expand Up @@ -73,8 +74,8 @@ const $redisForSub: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisForPub, $redisForSub],
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
providers: [$config, $db, $redis, $redisForPub, $redisForSub, MeiliService],
exports: [$config, $db, $redis, $redisForPub, $redisForSub, MeiliService, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
Expand Down
41 changes: 41 additions & 0 deletions packages/backend/src/MeiliService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MeiliSearch, Index } from 'meilisearch';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { Note } from '@/models/entities/Note.js';

@Injectable()
export class MeiliService implements OnModuleInit {
public index: Index | null = null;
private client: MeiliSearch;
constructor(
@Inject(DI.config) private config: Config,
) { }

async onModuleInit(): Promise<void> {
if (this.config.meilisearch) {
this.client = new MeiliSearch({
host: `http://${this.config.meilisearch.host}:${this.config.meilisearch.port}`,
apiKey: this.config.meilisearch.apiKey,
});
this.index = this.client.index(this.config.meilisearch.index);
this.index.updateSettings(
this.config.meilisearch.config,
);
}
}

addNote (note: Note): void {
if ( this.index !== null && !(note.renoteId && !note.text) ) {
this.index.addDocuments([
{
id: note.id,
createdAt: note.createdAt,
text: note.text,
cw: note.cw,
userHost: note.userHost,
},
]);
}
}
}
17 changes: 17 additions & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ export type Source = {
pass?: string;
index?: string;
};
meilisearch?: {
host: string;
port: string;
apiKey: string;
index: string;
searchType?: 'last' | 'all';
indexLocalOnly?: boolean;
config: {
rankingRules?: string[];
searchableAttributes?: string[];
typoTolerance?: object; // https://www.meilisearch.com/docs/reference/api/settings#typo-tolerance
pagination?: {
maxTotalHits: number;
}
};

};

proxy?: string;
proxySmtp?: string;
Expand Down
9 changes: 8 additions & 1 deletion packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { MeiliService } from '@/MeiliService.js';

const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);

Expand Down Expand Up @@ -180,7 +181,8 @@ export class NoteCreateService implements OnApplicationShutdown {

@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,


private meiliService: MeiliService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
Expand Down Expand Up @@ -424,6 +426,11 @@ export class NoteCreateService implements OnApplicationShutdown {
await this.notesRepository.insert(insert);
}

// 全投稿をインデックスに追加する
if (this.config.meilisearch && (!this.config.meilisearch.indexLocalOnly || insert.localOnly)) {
this.meiliService.addNote(insert);
}

return insert;
} catch (e) {
// duplicate key error
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/di-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const DI = {
redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'),
meiliService: Symbol('meiliService'),

//#region Repositories
usersRepository: Symbol('usersRepository'),
Expand Down
38 changes: 37 additions & 1 deletion packages/backend/src/server/api/endpoints/notes/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { Note } from '@/models/entities/Note.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { RoleService } from '@/core/RoleService.js';
import { MeiliService } from '@/MeiliService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -48,6 +50,7 @@ export const paramDef = {
},
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
page: { type: 'integer', minimum: 1, default: 1 },
},
required: ['query'],
} as const;
Expand All @@ -64,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

private meiliService: MeiliService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
Expand All @@ -73,7 +77,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (!policies.canSearchNotes) {
throw new ApiError(meta.errors.unavailable);
}


if (meiliService.index !== null) {
const meili = await meiliService.index.search(ps.query, {
sort: ['createdAt:desc'],
attributesToRetrieve: ['id', 'createdAt'],
matchingStrategy: 'all',
page: ps.page,
});

const fastNotes: Note[] = [];
for (const [_, v] of Object.entries(meili.hits)) {
const query = this.notesRepository.createQueryBuilder('note');
query.andWhere('note.id = :id', { id: v.id })
.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);
this.queryService.generateBlockedUserQuery(query, me);
}
const note = await query.getOne();
if (note) {
fastNotes.push(note);
}
}

return await this.noteEntityService.packMany(fastNotes, me);
}

const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);

if (ps.userId) {
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/components/MkPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ let isPausingUpdate = false;
let timerForSetPause: number | null = null;
const BACKGROUND_PAUSE_WAIT_SEC = 10;

let pagedCount = 1;

// 先頭が表示されているかどうかを検出
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
let scrollObserver = $ref<IntersectionObserver>();
Expand Down Expand Up @@ -164,6 +166,7 @@ async function init(): Promise<void> {
await os.api(props.pagination.endpoint, {
...params,
limit: props.pagination.limit ?? 10,
page: pagedCount++,
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
Expand Down Expand Up @@ -204,6 +207,7 @@ const fetchMore = async (): Promise<void> => {
offset: offset.value,
} : {
untilId: items.value[items.value.length - 1].id,
page: pagedCount++,
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
Expand Down
12 changes: 11 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.