diff options
| author | MohamedBassem <me@mbassem.com> | 2025-07-27 19:37:11 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2025-07-27 19:37:11 +0100 |
| commit | b94896a0f8fa43b957a9bdd6ab57ada0ab8101af (patch) | |
| tree | ed8f79ce7d407379fa0d8210db52959f849fac0e /packages/plugins-search-meilisearch | |
| parent | 7bb7f18fbf8e374efde2fe28bacfc29157b9fa19 (diff) | |
| download | karakeep-b94896a0f8fa43b957a9bdd6ab57ada0ab8101af.tar.zst | |
refactor: Extract meilisearch as a plugin
Diffstat (limited to 'packages/plugins-search-meilisearch')
| -rw-r--r-- | packages/plugins-search-meilisearch/.oxlintrc.json | 19 | ||||
| -rw-r--r-- | packages/plugins-search-meilisearch/index.ts | 12 | ||||
| -rw-r--r-- | packages/plugins-search-meilisearch/package.json | 26 | ||||
| -rw-r--r-- | packages/plugins-search-meilisearch/src/env.ts | 8 | ||||
| -rw-r--r-- | packages/plugins-search-meilisearch/src/index.ts | 174 | ||||
| -rw-r--r-- | packages/plugins-search-meilisearch/tsconfig.json | 9 |
6 files changed, 248 insertions, 0 deletions
diff --git a/packages/plugins-search-meilisearch/.oxlintrc.json b/packages/plugins-search-meilisearch/.oxlintrc.json new file mode 100644 index 00000000..79ba0255 --- /dev/null +++ b/packages/plugins-search-meilisearch/.oxlintrc.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/oxlint/configuration_schema.json", + "extends": [ + "../../tooling/oxlint/oxlint-base.json" + ], + "env": { + "builtin": true, + "commonjs": true + }, + "ignorePatterns": [ + "**/*.config.js", + "**/*.config.cjs", + "**/.eslintrc.cjs", + "**/.next", + "**/dist", + "**/build", + "**/pnpm-lock.yaml" + ] +} diff --git a/packages/plugins-search-meilisearch/index.ts b/packages/plugins-search-meilisearch/index.ts new file mode 100644 index 00000000..3496d52f --- /dev/null +++ b/packages/plugins-search-meilisearch/index.ts @@ -0,0 +1,12 @@ +// Auto-register the MeiliSearch provider when this package is imported +import { PluginManager, PluginType } from "@karakeep/shared/plugins"; + +import { MeiliSearchProvider } from "./src"; + +if (MeiliSearchProvider.isConfigured()) { + PluginManager.register({ + type: PluginType.Search, + name: "MeiliSearch", + provider: new MeiliSearchProvider(), + }); +} diff --git a/packages/plugins-search-meilisearch/package.json b/packages/plugins-search-meilisearch/package.json new file mode 100644 index 00000000..3bc9db80 --- /dev/null +++ b/packages/plugins-search-meilisearch/package.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@karakeep/plugins-search-meilisearch", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "format": "prettier . --ignore-path ../../.prettierignore", + "format:fix": "prettier . --write --ignore-path ../../.prettierignore", + "lint": "oxlint .", + "lint:fix": "oxlint . --fix", + "test": "vitest" + }, + "dependencies": { + "@karakeep/shared": "workspace:*", + "meilisearch": "^0.45.0" + }, + "devDependencies": { + "@karakeep/prettier-config": "workspace:^0.1.0", + "@karakeep/tsconfig": "workspace:^0.1.0", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^3.2.4" + }, + "prettier": "@karakeep/prettier-config" +} diff --git a/packages/plugins-search-meilisearch/src/env.ts b/packages/plugins-search-meilisearch/src/env.ts new file mode 100644 index 00000000..c06fdd55 --- /dev/null +++ b/packages/plugins-search-meilisearch/src/env.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const envConfig = z + .object({ + MEILI_ADDR: z.string().optional(), + MEILI_MASTER_KEY: z.string().default(""), + }) + .parse(process.env); diff --git a/packages/plugins-search-meilisearch/src/index.ts b/packages/plugins-search-meilisearch/src/index.ts new file mode 100644 index 00000000..6153a9c4 --- /dev/null +++ b/packages/plugins-search-meilisearch/src/index.ts @@ -0,0 +1,174 @@ +import type { Index } from "meilisearch"; +import { MeiliSearch } from "meilisearch"; + +import type { + BookmarkSearchDocument, + SearchIndexClient, + SearchOptions, + SearchResponse, +} from "@karakeep/shared/search"; +import { PluginProvider } from "@karakeep/shared/plugins"; + +import { envConfig } from "./env"; + +class MeiliSearchIndexClient implements SearchIndexClient { + constructor(private index: Index<BookmarkSearchDocument>) {} + + async addDocuments(documents: BookmarkSearchDocument[]): Promise<void> { + const task = await this.index.addDocuments(documents, { + primaryKey: "id", + }); + await this.index.waitForTask(task.taskUid); + const taskResult = await this.index.getTask(task.taskUid); + if (taskResult.error) { + throw new Error( + `MeiliSearch add documents failed: ${taskResult.error.message}`, + ); + } + } + + async updateDocuments(documents: BookmarkSearchDocument[]): Promise<void> { + const task = await this.index.updateDocuments(documents, { + primaryKey: "id", + }); + await this.index.waitForTask(task.taskUid); + const taskResult = await this.index.getTask(task.taskUid); + if (taskResult.error) { + throw new Error( + `MeiliSearch update documents failed: ${taskResult.error.message}`, + ); + } + } + + async deleteDocument(id: string): Promise<void> { + const task = await this.index.deleteDocument(id); + await this.index.waitForTask(task.taskUid); + const taskResult = await this.index.getTask(task.taskUid); + if (taskResult.error) { + throw new Error( + `MeiliSearch delete document failed: ${taskResult.error.message}`, + ); + } + } + + async deleteDocuments(ids: string[]): Promise<void> { + const task = await this.index.deleteDocuments(ids); + await this.index.waitForTask(task.taskUid); + const taskResult = await this.index.getTask(task.taskUid); + if (taskResult.error) { + throw new Error( + `MeiliSearch delete documents failed: ${taskResult.error.message}`, + ); + } + } + + async search(options: SearchOptions): Promise<SearchResponse> { + const result = await this.index.search(options.query, { + filter: options.filter, + limit: options.limit, + offset: options.offset, + sort: options.sort, + }); + + return { + hits: result.hits.map((hit) => ({ + id: hit.id, + score: hit._rankingScore, + })), + totalHits: result.estimatedTotalHits ?? 0, + processingTimeMs: result.processingTimeMs, + }; + } + + async clearIndex(): Promise<void> { + const task = await this.index.deleteAllDocuments(); + await this.index.waitForTask(task.taskUid); + const taskResult = await this.index.getTask(task.taskUid); + if (taskResult.error) { + throw new Error( + `MeiliSearch clear index failed: ${taskResult.error.message}`, + ); + } + } +} + +export class MeiliSearchProvider implements PluginProvider<SearchIndexClient> { + private client: MeiliSearch | undefined; + private indexClient: SearchIndexClient | undefined; + private readonly indexName = "bookmarks"; + + constructor() { + if (MeiliSearchProvider.isConfigured()) { + this.client = new MeiliSearch({ + host: envConfig.MEILI_ADDR!, + apiKey: envConfig.MEILI_MASTER_KEY, + }); + } + } + + static isConfigured(): boolean { + return !!envConfig.MEILI_ADDR; + } + + async getClient(): Promise<SearchIndexClient | null> { + if (this.indexClient) { + return this.indexClient; + } + + if (!this.client) { + return null; + } + + const indices = await this.client.getIndexes(); + let indexFound = indices.results.find((i) => i.uid === this.indexName); + + if (!indexFound) { + const idx = await this.client.createIndex(this.indexName, { + primaryKey: "id", + }); + await this.client.waitForTask(idx.taskUid); + indexFound = await this.client.getIndex<BookmarkSearchDocument>( + this.indexName, + ); + } + + await this.configureIndex(indexFound); + this.indexClient = new MeiliSearchIndexClient(indexFound); + return this.indexClient; + } + + private async configureIndex( + index: Index<BookmarkSearchDocument>, + ): Promise<void> { + const desiredFilterableAttributes = ["id", "userId"].sort(); + const desiredSortableAttributes = ["createdAt"].sort(); + + const settings = await index.getSettings(); + + if ( + JSON.stringify(settings.filterableAttributes?.sort()) !== + JSON.stringify(desiredFilterableAttributes) + ) { + console.log( + `[meilisearch] Updating desired filterable attributes to ${desiredFilterableAttributes} from ${settings.filterableAttributes}`, + ); + const taskId = await index.updateFilterableAttributes( + desiredFilterableAttributes, + ); + await this.client!.waitForTask(taskId.taskUid); + } + + if ( + JSON.stringify(settings.sortableAttributes?.sort()) !== + JSON.stringify(desiredSortableAttributes) + ) { + console.log( + `[meilisearch] Updating desired sortable attributes to ${desiredSortableAttributes} from ${settings.sortableAttributes}`, + ); + const taskId = await index.updateSortableAttributes( + desiredSortableAttributes, + ); + await this.client!.waitForTask(taskId.taskUid); + } + } +} diff --git a/packages/plugins-search-meilisearch/tsconfig.json b/packages/plugins-search-meilisearch/tsconfig.json new file mode 100644 index 00000000..a795b96a --- /dev/null +++ b/packages/plugins-search-meilisearch/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@karakeep/tsconfig/node.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + } +} |
