2023-05-25 09:53:04 +02:00
|
|
|
import {Health, MeiliSearch, Stats } from 'meilisearch';
|
2023-05-25 00:55:33 +02:00
|
|
|
import { dbLogger } from "./logger.js";
|
|
|
|
|
|
|
|
import config from "@/config/index.js";
|
2023-05-25 23:53:08 +02:00
|
|
|
import {Note} from "@/models/entities/note.js";
|
2023-05-26 00:33:02 +02:00
|
|
|
import * as url from "url";
|
2023-05-25 00:55:33 +02:00
|
|
|
|
|
|
|
const logger = dbLogger.createSubLogger("meilisearch", "gray", false);
|
|
|
|
|
|
|
|
logger.info("Connecting to MeiliSearch");
|
|
|
|
|
|
|
|
const hasConfig =
|
|
|
|
config.meilisearch && (config.meilisearch.host || config.meilisearch.port || config.meilisearch.apiKey);
|
|
|
|
|
|
|
|
const host = hasConfig ? config.meilisearch.host ?? "localhost" : "";
|
|
|
|
const port = hasConfig ? config.meilisearch.port ?? 7700 : 0;
|
|
|
|
const auth = hasConfig ? config.meilisearch.apiKey ?? "" : "";
|
|
|
|
|
2023-05-25 14:15:13 +02:00
|
|
|
const client: MeiliSearch = new MeiliSearch({
|
2023-05-25 02:19:42 +02:00
|
|
|
host: `http://${host}:${port}`,
|
|
|
|
apiKey: auth,
|
2023-05-25 00:55:33 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
const posts = client.index('posts');
|
|
|
|
|
2023-05-25 22:29:47 +02:00
|
|
|
posts.updateSearchableAttributes(['text']).catch((e) => logger.error(`Setting searchable attr failed, searches won't work: ${e}`));
|
2023-05-25 00:55:33 +02:00
|
|
|
|
2023-05-25 22:29:47 +02:00
|
|
|
posts.updateFilterableAttributes(["userName", "userHost", "mediaAttachment", "createdAt"]).catch((e) => logger.error(`Setting filterable attr failed, advanced searches won't work: ${e}`));
|
2023-05-25 14:15:13 +02:00
|
|
|
|
2023-05-25 00:55:33 +02:00
|
|
|
logger.info("Connected to MeiliSearch");
|
|
|
|
|
|
|
|
export type MeilisearchNote = {
|
|
|
|
id: string;
|
|
|
|
text: string;
|
|
|
|
userId: string;
|
|
|
|
userHost: string;
|
2023-05-25 22:29:47 +02:00
|
|
|
userName: string;
|
2023-05-25 00:55:33 +02:00
|
|
|
channelId: string;
|
2023-05-25 14:15:13 +02:00
|
|
|
mediaAttachment: string;
|
2023-05-25 22:29:47 +02:00
|
|
|
createdAt: number
|
2023-05-25 00:55:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default hasConfig ? {
|
|
|
|
search: (query : string, limit : number, offset : number) => {
|
|
|
|
|
2023-05-25 14:15:13 +02:00
|
|
|
/// Advanced search syntax
|
|
|
|
/// from:user => filter by user + optional domain
|
|
|
|
/// has:image/video/audio/text/file => filter by attachment types
|
|
|
|
/// domain:domain.com => filter by domain
|
2023-05-25 22:29:47 +02:00
|
|
|
/// before:Date => show posts made before Date
|
|
|
|
/// after: Date => show posts made after Date
|
|
|
|
|
2023-05-25 14:15:13 +02:00
|
|
|
|
|
|
|
let constructedFilters: string[] = [];
|
|
|
|
|
|
|
|
let splitSearch = query.split(" ");
|
2023-05-25 22:29:47 +02:00
|
|
|
|
|
|
|
// Detect search operators and remove them from the actual query
|
|
|
|
splitSearch.filter(term => {
|
2023-05-25 14:15:13 +02:00
|
|
|
if (term.startsWith("has:")) {
|
|
|
|
let fileType = term.slice(4);
|
|
|
|
constructedFilters.push(`mediaAttachment = "${fileType}"`)
|
2023-05-25 22:29:47 +02:00
|
|
|
return false;
|
|
|
|
} else if (term.startsWith("from:")) {
|
2023-05-25 14:15:13 +02:00
|
|
|
let user = term.slice(5);
|
2023-05-25 22:29:47 +02:00
|
|
|
constructedFilters.push(`userName = ${user}`)
|
|
|
|
return false;
|
|
|
|
} else if (term.startsWith("domain:")) {
|
2023-05-25 14:15:13 +02:00
|
|
|
let domain = term.slice(7);
|
|
|
|
constructedFilters.push(`userHost = ${domain}`)
|
2023-05-25 22:29:47 +02:00
|
|
|
return false;
|
|
|
|
} else if (term.startsWith("after:")) {
|
|
|
|
let timestamp = term.slice(6);
|
|
|
|
// Try to parse the timestamp as JavaScript Date
|
|
|
|
let date = Date.parse(timestamp);
|
|
|
|
if (isNaN(date)) return false;
|
|
|
|
constructedFilters.push(`createdAt > ${date}`)
|
|
|
|
} else if (term.startsWith("before:")) {
|
|
|
|
let timestamp = term.slice(7);
|
|
|
|
// Try to parse the timestamp as JavaScript Date
|
|
|
|
let date = Date.parse(timestamp);
|
|
|
|
if (isNaN(date)) return false;
|
|
|
|
constructedFilters.push(`createdAt < ${date}`)
|
2023-05-25 14:15:13 +02:00
|
|
|
}
|
2023-05-25 22:29:47 +02:00
|
|
|
|
|
|
|
return true;
|
2023-05-25 14:15:13 +02:00
|
|
|
})
|
|
|
|
|
2023-05-25 22:37:43 +02:00
|
|
|
logger.info(`Searching for ${query}`);
|
|
|
|
logger.info(`Limit: ${limit}`);
|
|
|
|
logger.info(`Offset: ${offset}`);
|
|
|
|
logger.info(`Filters: ${constructedFilters}`)
|
|
|
|
|
|
|
|
|
|
|
|
return posts.search(splitSearch.join(" "), {
|
2023-05-25 00:55:33 +02:00
|
|
|
limit: limit,
|
|
|
|
offset: offset,
|
2023-05-25 14:15:13 +02:00
|
|
|
filter: constructedFilters
|
2023-05-25 00:55:33 +02:00
|
|
|
});
|
|
|
|
},
|
2023-05-25 23:49:52 +02:00
|
|
|
ingestNote: (note: Note | Note[]) => {
|
|
|
|
if (note instanceof Note) {
|
|
|
|
note = [note];
|
2023-05-25 14:15:13 +02:00
|
|
|
}
|
|
|
|
|
2023-05-25 23:49:52 +02:00
|
|
|
let indexingBatch: MeilisearchNote[] = [];
|
|
|
|
|
|
|
|
note.forEach(note => {
|
|
|
|
|
|
|
|
let attachmentType = "";
|
|
|
|
if (note.attachedFileTypes.length > 0) {
|
|
|
|
attachmentType = note.attachedFileTypes[0].split("/")[0];
|
|
|
|
switch (attachmentType) {
|
|
|
|
case "image":
|
|
|
|
case "video":
|
|
|
|
case "audio":
|
|
|
|
case "text":
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
attachmentType = "file"
|
|
|
|
break
|
|
|
|
}
|
2023-05-25 00:55:33 +02:00
|
|
|
}
|
2023-05-25 23:49:52 +02:00
|
|
|
|
2023-05-26 00:33:02 +02:00
|
|
|
indexingBatch.push(<MeilisearchNote>{
|
2023-05-25 23:49:52 +02:00
|
|
|
id: note.id.toString(),
|
|
|
|
text: note.text ? note.text : "",
|
|
|
|
userId: note.userId,
|
2023-05-26 00:33:02 +02:00
|
|
|
userHost: note.userHost !== "" ? note.userHost : url.parse(config.host).host,
|
2023-05-25 23:49:52 +02:00
|
|
|
channelId: note.channelId ? note.channelId : "",
|
|
|
|
mediaAttachment: attachmentType,
|
2023-05-26 00:33:02 +02:00
|
|
|
userName: note.user?.usernameLower ?? "UNKNOWN",
|
2023-05-25 23:49:52 +02:00
|
|
|
createdAt: note.createdAt.getTime() / 1000 // division by 1000 is necessary because Node returns in ms-accuracy
|
|
|
|
}
|
|
|
|
)
|
|
|
|
});
|
|
|
|
|
|
|
|
let indexingIDs = indexingBatch.map(note => note.id);
|
|
|
|
|
|
|
|
logger.info("Indexing notes in MeiliSearch: " + indexingIDs.join(","));
|
|
|
|
|
2023-05-26 00:04:07 +02:00
|
|
|
return posts.addDocuments(indexingBatch, {
|
|
|
|
primaryKey: "id"
|
|
|
|
});
|
2023-05-25 00:55:33 +02:00
|
|
|
},
|
2023-05-25 09:53:04 +02:00
|
|
|
serverStats: async () => {
|
|
|
|
let health : Health = await client.health();
|
|
|
|
let stats: Stats = await client.getStats();
|
|
|
|
|
|
|
|
return {
|
|
|
|
health: health.status,
|
|
|
|
size: stats.databaseSize,
|
|
|
|
indexed_count: stats.indexes["posts"].numberOfDocuments
|
|
|
|
}
|
|
|
|
}
|
2023-05-25 00:55:33 +02:00
|
|
|
} : null;
|