@crowi/plugin-search-opensearch 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/index.d.mts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +835 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +805 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/driver.ts","../src/parse-query.ts","../src/query-builder.ts","../src/mappings/default.json","../src/mappings/kuromoji.json","../src/mappings/sudachi.json"],"sourcesContent":["/**\n * @crowi/plugin-search-opensearch — search driver registering\n * `'opensearch'` against the SearchRegistry.\n *\n * Activation: add this plugin to the runner's `crowi.config.json`\n * `plugins` array and set `search.driver: 'opensearch'`. Configure\n * via the Mongo Config namespace `plugin:@crowi/plugin-search-opensearch:*`\n * — operators set the connection URL exclusively from the admin UI.\n */\n\nimport { z } from 'zod/v3';\nimport type { CrowiPlugin, PluginContext } from '@crowi/plugin-api';\nimport {\n applyConfig,\n applyConfigInPlace,\n createOpenSearchDriver,\n type Analyzer,\n type OpenSearchDriver,\n type OpenSearchDriverConfig,\n type OSDriverState,\n type PageStreamDoc,\n} from './driver';\n\nexport { applyConfig, applyConfigInPlace, createOpenSearchDriver } from './driver';\nexport type { OpenSearchDriver, OpenSearchDriverConfig, OpenSearchDriverDeps, OSDriverState, PageStreamDoc, Analyzer } from './driver';\nexport { parseQuery } from './parse-query';\nexport { buildSearchBody } from './query-builder';\n\nexport const OpenSearchConfigSchema = z\n .object({\n /**\n * `https://[user:pass@]host[:port][/indexName]`. Empty string keeps\n * the driver registered but disabled — `query()` will throw a\n * helpful error and `index()` becomes a no-op.\n *\n * Marked `@sensitive` because the URL embeds the cluster password\n * (Bonsai-style `https://USER:PASS@HOST/INDEX`); we don't want\n * Mongo to keep it in plaintext.\n */\n url: z.string().describe('@sensitive OpenSearch endpoint (https://USER:PASS@HOST/INDEX format).').default(''),\n /**\n * Base index name. Used as the `indexName` if not provided in the\n * URL path. The runtime alias `${indexName}-current` is what the\n * driver actually targets for read / write.\n */\n indexName: z.string().default('crowi'),\n requestTimeout: z.number().int().positive().default(5000),\n /**\n * Mapping flavour. Cluster requirements:\n * - `default`: no extra OpenSearch plugin.\n * - `kuromoji`: `analysis-kuromoji` plugin (Apache 2.0, a\n * separate distribution from OpenSearch core — install via\n * `bin/opensearch-plugin install analysis-kuromoji`).\n * - `sudachi`: `analysis-sudachi` (OpenSearch-compatible fork\n * from WorksApplications) + dictionary; operators must build\n * a derived image. Picking this without the plugin makes\n * `rebuild()` fail.\n */\n analyzer: z\n .enum(['default', 'kuromoji', 'sudachi'])\n .describe('default / kuromoji (analysis-kuromoji plugin) / sudachi (analysis-sudachi plugin + dictionary, custom image required)')\n .default('default'),\n })\n .strict();\n\nexport type OpenSearchConfig = z.infer<typeof OpenSearchConfigSchema>;\n\nconst PLUGIN_NAME = '@crowi/plugin-search-opensearch';\n\n/**\n * Module-scope driver state ref. `registerSearch` initialises it from\n * the boot-time config and registers a driver bound to it; the driver\n * methods snapshot from it on every call; `reconfigure` mutates its\n * fields in place when admin saves new connection / index / analyzer /\n * requestTimeout values. The single-instance assumption is fine — the\n * plugin registers exactly one `'opensearch'` driver, owned by this\n * module. `null` before `registerSearch` runs.\n */\nlet state: OSDriverState | null = null;\n\nfunction toDriverConfig(config: OpenSearchConfig): OpenSearchDriverConfig {\n return {\n url: config.url,\n indexName: config.indexName,\n requestTimeout: config.requestTimeout,\n analyzer: config.analyzer,\n };\n}\n\nconst plugin: CrowiPlugin = {\n name: PLUGIN_NAME,\n version: '0.1.0-dev',\n configSchema: OpenSearchConfigSchema,\n adminPlacement: {\n label: 'OpenSearch',\n icon: 'search',\n // section omitted: derived from registerSearch -> 'shared' fallback\n },\n\n registerSearch: (registry, ctx) => {\n const config = ctx.config<OpenSearchConfig>();\n\n if (!config.url) {\n ctx.log.warn('url is empty; the opensearch search driver is disabled until configured.');\n // NOTE: empty-url -> configured-url is restart-only. The driver\n // is not registered here, so `reconfigure` has nothing to mutate\n // back into; the operator restarts after first configuring a url.\n return;\n }\n\n state = applyConfig(toDriverConfig(config));\n const driver = buildDriver(state, ctx);\n registry.register('opensearch', driver);\n ctx.log.debug('registered opensearch search driver (node=%s, indexName=%s, analyzer=%s)', driver.node, driver.baseIndexName, config.analyzer);\n },\n\n reconfigure: (ctx) => {\n if (!state) {\n // registerSearch skipped (boot-time url was empty). Configuring a\n // url now needs a restart — see the registerSearch note above.\n ctx.log.warn('reconfigure: driver was not registered at boot (url was empty); a server restart is required to enable OpenSearch search.');\n return;\n }\n const config = ctx.config<OpenSearchConfig>();\n if (!config.url) {\n ctx.log.warn('reconfigure: url cleared; search requests will fail with a \"Search not configured\" error until a url is set.');\n }\n const { oldClient } = applyConfigInPlace(state, toDriverConfig(config));\n // Fire-and-forget: the OpenSearch Client keeps an HTTP keep-alive\n // pool, so the old one must be closed — but awaiting it would block\n // the admin save response. Inflight requests already snapshotted\n // the old client and drain to completion regardless.\n if (oldClient) {\n void oldClient.close().catch((err: unknown) => {\n ctx.log.warn('reconfigure: closing the previous OpenSearch client failed: %o', err);\n });\n }\n ctx.log.debug('reconfigured opensearch search driver (node=%s, index=%s, analyzer=%s)', state.node || '<unset>', state.baseIndexName, config.analyzer);\n },\n};\n\nexport default plugin;\n\n// ---------------------------------------------------------------------------\n// Wiring helpers — visible for tests.\n// ---------------------------------------------------------------------------\n\ninterface PageModelLike {\n allPageCount: () => Promise<number>;\n getStreamOfFindAll: (opts: { publicOnly: boolean }) => {\n eachAsync: (handler: (doc: PageStreamDoc) => Promise<void>) => Promise<void>;\n };\n}\n\ninterface BookmarkAggregateRow {\n _id: { toString: () => string } | string;\n n: number;\n}\n\ninterface BookmarkModelLike {\n aggregate: (pipeline: unknown[]) => Promise<BookmarkAggregateRow[]>;\n}\n\ninterface UserModelLike {\n countDocuments: (q?: unknown) => { exec: () => Promise<number> };\n}\n\nfunction buildDriver(driverState: OSDriverState, ctx: PluginContext): OpenSearchDriver {\n const Page = ctx.model('Page') as PageModelLike;\n const Bookmark = ctx.model('Bookmark') as BookmarkModelLike;\n const User = ctx.model('User') as UserModelLike;\n\n return createOpenSearchDriver(driverState, {\n log: ctx.log,\n iteratePages: async (handler) => {\n const cursor = Page.getStreamOfFindAll({ publicOnly: false });\n await cursor.eachAsync(handler);\n },\n countAllPages: () => Page.allPageCount(),\n getBookmarkCountsBulk: async () => {\n // One Mongo aggregate -> Map<pageId, count>. Same shape as the\n // ES plugin uses; replaces the legacy per-doc\n // `Bookmark.countByPageId` lookup that was O(N) round-trips\n // during a full rebuild.\n const rows = await Bookmark.aggregate([{ $group: { _id: '$page', n: { $sum: 1 } } }]);\n const map = new Map<string, number>();\n for (const row of rows) {\n const key = typeof row._id === 'string' ? row._id : row._id.toString();\n map.set(key, row.n);\n }\n return map;\n },\n countUsers: () => User.countDocuments({}).exec(),\n });\n}\n","/**\n * OpenSearch driver implementing the `SearchDriver` contract. Owns\n * the Client, the `${indexName}-current` alias (legacy ops compat),\n * single-doc index / remove, query against the alias, and rebuild-\n * from-scratch in 2k-doc bulk batches with bookmark counts pre-fetched\n * in one aggregate. Document field shape (path / body / username /\n * grant / granted_users / *_count / *_at) matches the ES plugin's\n * shape so a cluster migration is a re-point + rebuild rather than a\n * mapping rewrite.\n *\n * SDK note: `@opensearch-project/opensearch` 3.x returns\n * `{ body, statusCode, ... }` wrappers around every API response (the\n * shape inherited from the old `elasticsearch-js` 7.x line). The\n * Elasticsearch 9 client we use for `@crowi/plugin-search-elasticsearch`\n * collapsed those wrappers — so every call site here unwraps `body`\n * explicitly. Bulk requests likewise take `{ body: operations }`, not\n * the ES 9 `{ operations }` keyword.\n */\n\nimport { Client } from '@opensearch-project/opensearch';\n\ntype ClientOptions = NonNullable<ConstructorParameters<typeof Client>[0]>;\nimport type { SearchDriver, SearchHits, SearchQuery, SearchableDoc, PluginLogger } from '@crowi/plugin-api';\nimport { parseQuery } from './parse-query';\nimport { buildSearchBody, type FunctionScoreParams } from './query-builder';\nimport defaultMapping from './mappings/default.json';\nimport kuromojiOverlay from './mappings/kuromoji.json';\nimport sudachiOverlay from './mappings/sudachi.json';\n\nexport type Analyzer = 'default' | 'kuromoji' | 'sudachi';\n\nexport interface OpenSearchDriverConfig {\n url: string;\n indexName: string;\n requestTimeout: number;\n analyzer: Analyzer;\n}\n\nexport interface OpenSearchDriverDeps {\n log?: PluginLogger;\n /**\n * Iterate every page in the Mongo Page collection in cursor-style.\n * Plugin can't import the Page model directly, so the manager wires\n * this in from `ctx.model('Page')`. Each yielded doc is the lean\n * shape produced by `Page.getStreamOfFindAll({ publicOnly: false })`.\n */\n iteratePages?: (handler: (page: PageStreamDoc) => Promise<void>) => Promise<void>;\n /** Total page count, used for progress reporting. */\n countAllPages?: () => Promise<number>;\n /**\n * Bulk-fetch bookmark counts for every page in one Mongo aggregate.\n * Avoids the per-doc N+1 lookup the legacy rebuild used. Returns a\n * `Map<pageId, count>`; pages without bookmarks may be absent\n * (caller defaults to 0).\n */\n getBookmarkCountsBulk?: () => Promise<Map<string, number>>;\n /** Total user count, used to scale the bookmark-count factor. */\n countUsers?: () => Promise<number>;\n}\n\n/** The lean Page document shape we expect from the rebuild stream. */\nexport interface PageStreamDoc {\n _id: { toString: () => string } | string;\n path: string;\n redirectTo: string | null;\n status: string;\n grant: number;\n grantedUsers?: Array<{ toString: () => string } | string>;\n creator?: { username?: string };\n revision?: { body?: string };\n liker?: unknown[];\n commentCount?: number;\n bookmarkCount?: number;\n createdAt?: Date;\n updatedAt?: Date;\n}\n\nexport interface OpenSearchDriver extends SearchDriver {\n /** Currently-targeted alias name (`<indexName>-current`). Exposed for tests / admin UI. */\n readonly aliasName: string;\n /** OpenSearch node URI parsed out of `config.url`. */\n readonly node: string;\n /** Base index name (without timestamp / `-current` suffix). */\n readonly baseIndexName: string;\n /** Test-only handle to the underlying client. */\n readonly client: Client;\n}\n\n/**\n * Mutable driver state. `createOpenSearchDriver` receives a ref to\n * this; each driver method snapshots the fields it needs *once at the\n * top* of the call, so a concurrent `reconfigure` cannot swap the\n * client / index name mid-operation. `reconfigure` mutates the fields\n * in place via {@link applyConfigInPlace}; the next call sees the new\n * values. An empty `url` leaves `client` as `null` — the methods then\n * throw a `Search not configured` error rather than touching a stale\n * client.\n */\nexport interface OSDriverState {\n /** `null` when `url` is empty (driver configured-but-disabled). */\n client: Client | null;\n /** OpenSearch node URI parsed out of `config.url`; empty string when `url` is empty. */\n node: string;\n /** Base index name (without timestamp / `-current` suffix). */\n baseIndexName: string;\n /** Runtime alias the driver reads / writes (`<baseIndexName>-current`). */\n aliasName: string;\n analyzer: Analyzer;\n requestTimeout: number;\n}\n\n/**\n * Build a fresh {@link OSDriverState} from a config. An empty `url`\n * yields a disabled state (`client: null`) instead of throwing — the\n * driver stays registered but every method rejects with a\n * `Search not configured` error.\n */\nexport function applyConfig(config: OpenSearchDriverConfig): OSDriverState {\n if (!config.url) {\n return {\n client: null,\n node: '',\n baseIndexName: config.indexName,\n aliasName: `${config.indexName}-current`,\n analyzer: config.analyzer,\n requestTimeout: config.requestTimeout,\n };\n }\n const { node, indexName } = parseUri(config.url);\n const clientOpts: ClientOptions = {\n node,\n requestTimeout: config.requestTimeout,\n };\n return {\n client: new Client(clientOpts),\n node,\n baseIndexName: indexName,\n aliasName: `${indexName}-current`,\n analyzer: config.analyzer,\n requestTimeout: config.requestTimeout,\n };\n}\n\n/**\n * Mutate `target` in place to reflect `config`. Used by `reconfigure`:\n * the old client reference is returned so the caller can `close()` it\n * (fire-and-forget) once the swap is done — inflight operations have\n * already snapshotted the old client and will run to completion.\n */\nexport function applyConfigInPlace(target: OSDriverState, config: OpenSearchDriverConfig): { oldClient: Client | null } {\n const oldClient = target.client;\n // Assign over the freshly-built state so a new OSDriverState field\n // propagates through reconfigure automatically — no manual field\n // list here to fall out of sync with applyConfig.\n Object.assign(target, applyConfig(config));\n return { oldClient };\n}\n\n/** TTL for the cached user count used by the bookmark-count function-score factor. */\nconst USER_COUNT_TTL_MS = 5 * 60 * 1000;\n\n/**\n * Build the search driver around an {@link OSDriverState} ref. Methods\n * snapshot `state` *once at the top* — a `reconfigure` running\n * concurrently with an inflight call cannot swap the client mid-call;\n * the next call sees the new client / index name.\n */\nexport function createOpenSearchDriver(state: OSDriverState, deps: OpenSearchDriverDeps = {}): OpenSearchDriver {\n const log = deps.log;\n\n // Cached user count: refreshed on miss, every USER_COUNT_TTL_MS.\n // Avoids hammering Mongo with a full collection count on every\n // search query (the count only feeds a function-score factor, so\n // a stale value is fine).\n let userCountCache: { value: number; at: number } | null = null;\n const getCachedUserCount = async (): Promise<number | null> => {\n if (!deps.countUsers) return null;\n const now = Date.now();\n if (userCountCache && now - userCountCache.at < USER_COUNT_TTL_MS) {\n return userCountCache.value;\n }\n const value = await deps.countUsers();\n userCountCache = { value, at: now };\n return value;\n };\n\n const driver: OpenSearchDriver = {\n // Getters off the state ref: `reconfigure` makes these mutable, so\n // they must always reflect the *current* state, not a boot-time\n // literal. Tests read `driver.client` to install fakes — since the\n // getter returns the same object reference, mutating its methods\n // still works.\n get aliasName() {\n return state.aliasName;\n },\n get node() {\n return state.node;\n },\n get baseIndexName() {\n return state.baseIndexName;\n },\n get client() {\n return requireClient(state.client);\n },\n\n async index(doc: SearchableDoc): Promise<void> {\n const { client, aliasName } = snapshot(state);\n const source = docToEsSource(doc);\n await client.index({\n index: aliasName,\n id: doc.id,\n body: source as unknown as Record<string, unknown>,\n });\n },\n\n async remove(id: string): Promise<void> {\n const { client, aliasName } = snapshot(state);\n try {\n await client.delete({ index: aliasName, id });\n } catch (err) {\n // Idempotent: a missing doc is fine. The OpenSearch client\n // throws a `ResponseError` with statusCode 404 when the id\n // doesn't exist; swallow only that case.\n if (isNotFoundError(err)) return;\n throw err;\n }\n },\n\n async query(q: SearchQuery): Promise<SearchHits> {\n const { client, aliasName } = snapshot(state);\n const page = q.page && q.page > 0 ? q.page : 1;\n const limit = clampLimit(q.limit);\n const from = (page - 1) * limit;\n\n let functionScore: FunctionScoreParams | undefined;\n const userCount = await getCachedUserCount();\n if (userCount !== null) {\n const factor = 10000 / (userCount || 1);\n functionScore = {\n fieldValueFactor: { field: 'bookmark_count', modifier: 'log1p', factor, missing: 0 },\n boostMode: 'sum',\n };\n }\n\n const body = buildSearchBody({\n parsed: parseQuery(q.q),\n pathPrefix: q.pathPrefix,\n viewer: q.viewer,\n grants: q.grants,\n functionScore,\n from,\n size: limit,\n });\n\n // OpenSearch 3.x SDK takes the request body under the `body` key\n // (unlike ES 9 which inlines the body fields). The SDK's TS types\n // for `body` are very loose, so cast through `unknown`.\n const response = await client.search({\n index: aliasName,\n body: body as unknown as Record<string, unknown>,\n } as unknown as Parameters<Client['search']>[0]);\n\n const payload = (response.body ?? {}) as unknown as OsSearchResponseBody;\n const totalRaw = payload.hits?.total;\n const total = typeof totalRaw === 'number' ? totalRaw : (totalRaw?.value ?? 0);\n\n const rawHits = (payload.hits?.hits ?? []) as unknown as OsHit[];\n const hits = rawHits.map((h) => {\n const source = (h._source ?? {}) as { path?: string };\n const snippet = pickSnippet(h.highlight);\n return {\n id: String(h._id),\n path: source.path ?? '',\n score: typeof h._score === 'number' ? h._score : undefined,\n ...(snippet ? { snippet } : {}),\n };\n });\n\n return { total, hits };\n },\n\n async rebuild(): Promise<void> {\n // Snapshot up front: a `reconfigure` mid-rebuild must not retarget\n // a long-running rebuild onto a different cluster / index name —\n // it runs to completion against the cluster it started on.\n const { client, aliasName, baseIndexName, analyzer } = snapshot(state);\n if (!deps.iteratePages || !deps.countAllPages || !deps.getBookmarkCountsBulk) {\n throw new Error('@crowi/plugin-search-opensearch: rebuild() requires iteratePages / countAllPages / getBookmarkCountsBulk deps.');\n }\n\n const newIndexName = createTimestampedIndexName(baseIndexName);\n log?.info('rebuild: creating index %s', newIndexName);\n\n const mapping = loadMapping(analyzer);\n await client.indices.create({ index: newIndexName, body: mapping as unknown as Record<string, unknown> });\n\n log?.info('rebuild: prefetching bookmark counts');\n const bookmarkCounts = await deps.getBookmarkCountsBulk();\n\n log?.info('rebuild: indexing all pages');\n await indexAllPages({\n client,\n indexTarget: newIndexName,\n iteratePages: deps.iteratePages,\n countAllPages: deps.countAllPages,\n bookmarkCounts,\n log,\n });\n\n log?.info('rebuild: switching alias %s -> %s', aliasName, newIndexName);\n await switchAlias(client, aliasName, newIndexName);\n\n log?.info('rebuild: cleaning up old indices');\n await deleteOldIndices(client, baseIndexName, newIndexName);\n },\n };\n\n return driver;\n}\n\n/**\n * Thrown by every driver method when `url` is empty (the driver is\n * registered-but-disabled). Surfaces to the search route as a clear\n * \"configure OpenSearch\" error rather than a `TypeError` on `null`.\n */\nconst SEARCH_NOT_CONFIGURED = '@crowi/plugin-search-opensearch: Search not configured (OpenSearch url is empty).';\n\nfunction requireClient(client: Client | null): Client {\n if (!client) {\n throw new Error(SEARCH_NOT_CONFIGURED);\n }\n return client;\n}\n\n/**\n * One-line snapshot taken at the top of every driver method. Reading\n * `state` exactly once per call is what makes a concurrent\n * `reconfigure` race-safe.\n */\nfunction snapshot(state: OSDriverState): { client: Client; aliasName: string; baseIndexName: string; analyzer: Analyzer } {\n return {\n client: requireClient(state.client),\n aliasName: state.aliasName,\n baseIndexName: state.baseIndexName,\n analyzer: state.analyzer,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_LIMIT = 50;\nconst MAX_LIMIT = 200;\n\nfunction clampLimit(limit: number | undefined): number {\n if (!limit || limit <= 0) return DEFAULT_LIMIT;\n return Math.min(limit, MAX_LIMIT);\n}\n\n/**\n * A Bonsai-style URL follows the format\n * `https://{ID}:{PASSWORD}@{HOST}[/{indexName}]`. Returns the node\n * URL for the SDK and the base index name.\n */\nexport function parseUri(uri: string): { node: string; indexName: string } {\n if (!uri.startsWith('http')) {\n throw new Error('URL for OpenSearch should starts with http/https');\n }\n\n const osUrl = new URL(uri);\n const auth = osUrl.username && osUrl.password ? `${osUrl.username}:${osUrl.password}@` : '';\n const node = `${osUrl.protocol}//${auth}${osUrl.host}`;\n const indexName = osUrl.pathname && osUrl.pathname !== '/' ? osUrl.pathname.substring(1) : 'crowi';\n\n return { node, indexName };\n}\n\n/**\n * Index name format: `<base>-<utc-timestamp>-<random>`. The random\n * suffix prevents collision when two rebuilds start in the same\n * millisecond (rare but possible in CI / test setups). The format is\n * matched against `TS_INDEX_RE` in `deleteOldIndices` to guard against\n * accidentally deleting unrelated indices that happen to share the\n * `<base>-` prefix.\n */\nfunction createTimestampedIndexName(base: string): string {\n const d = new Date();\n const pad = (n: number, w = 2): string => String(n).padStart(w, '0');\n const ts = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}${pad(d.getUTCMilliseconds(), 3)}`;\n const rnd = Math.random().toString(36).slice(2, 6).padEnd(4, '0');\n return `${base}-${ts}-${rnd}`;\n}\n\n/** Matches the index name format produced by `createTimestampedIndexName`. */\nconst TS_INDEX_RE = /^.+-\\d{17}-[a-z0-9]{4}$/;\n\ntype Mapping = Record<string, unknown>;\n\n/**\n * Resolve the mapping for an analyzer. The `default.json` mapping is\n * the base; `kuromoji.json` / `sudachi.json` are overlays that add\n * the `*.ja` analyzer fields (and sudachi additionally registers the\n * `sudachi_*` analyzer / tokenizer in `settings.analysis`). The merge\n * is a small deep-merge of plain JSON objects — adequate for the\n * mapping shapes which are nested-object-only.\n */\nfunction loadMapping(analyzer: Analyzer): Mapping {\n const base = defaultMapping as Mapping;\n if (analyzer === 'default') return base;\n const overlay = (analyzer === 'kuromoji' ? kuromojiOverlay : sudachiOverlay) as Mapping;\n return deepMergeMappings(base, overlay);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction deepMergeMappings(a: Mapping, b: Mapping): Mapping {\n const out: Record<string, unknown> = { ...a };\n for (const key of Object.keys(b)) {\n const av = a[key];\n const bv = b[key];\n if (isPlainObject(av) && isPlainObject(bv)) {\n out[key] = deepMergeMappings(av, bv);\n } else {\n out[key] = bv;\n }\n }\n return out;\n}\n\ninterface BulkOp {\n index?: { _index: string; _id: string };\n delete?: { _index: string; _id: string };\n}\n\ninterface PageRebuildContext {\n client: Client;\n indexTarget: string;\n iteratePages: NonNullable<OpenSearchDriverDeps['iteratePages']>;\n countAllPages: NonNullable<OpenSearchDriverDeps['countAllPages']>;\n bookmarkCounts: Map<string, number>;\n log?: PluginLogger;\n}\n\nasync function indexAllPages(ctx: PageRebuildContext): Promise<void> {\n const allPageCount = await ctx.countAllPages();\n let operations: Array<BulkOp | Record<string, unknown>> = [];\n let total = 0;\n let skipped = 0;\n\n const flush = async (): Promise<void> => {\n if (operations.length === 0) return;\n try {\n // OpenSearch 3.x SDK: bulk operations go under `body`, not\n // `operations`. The response is `{ body: { errors, took, items, ... } }`.\n const response = await ctx.client.bulk({\n body: operations,\n timeout: '1d',\n });\n const payload = (response.body ?? {}) as { errors?: boolean; took?: number };\n if (payload.errors) {\n ctx.log?.warn('rebuild: bulk had item-level errors (took=%dms)', payload.took);\n }\n } catch (err) {\n ctx.log?.error('rebuild: bulk failed: %o', err);\n }\n operations = [];\n };\n\n await ctx.iteratePages(async (doc: PageStreamDoc) => {\n if (!doc.creator || !doc.revision || !shouldIndex(doc)) {\n skipped++;\n return;\n }\n total++;\n\n const id = typeof doc._id === 'string' ? doc._id : doc._id.toString();\n const bookmarkCount = ctx.bookmarkCounts.get(id) ?? 0;\n const source = pageStreamDocToEsSource(doc, bookmarkCount);\n\n operations.push({ index: { _index: ctx.indexTarget, _id: id } });\n operations.push(source as unknown as Record<string, unknown>);\n\n // Flush every 2000 documents (each doc = 2 operations).\n if (operations.length >= 4000) {\n await flush();\n }\n });\n\n await flush();\n ctx.log?.info('rebuild: indexed total=%d skipped=%d (allPageCount=%d)', total, skipped, allPageCount);\n}\n\nexport function shouldIndex(doc: PageStreamDoc): boolean {\n if (doc.redirectTo !== null && doc.redirectTo !== undefined) return false;\n if (doc.status === 'deleted') return false;\n // Drafts must never be indexed: the search route has no per-viewer\n // draft-author filter, so an indexed draft would leak its path/existence\n // to other users. Pages are (re)indexed on the update event at publish.\n if (doc.status === 'draft') return false;\n return true;\n}\n\nasync function switchAlias(client: Client, aliasName: string, newIndex: string): Promise<void> {\n const aliasInfo = await getCurrentAliasInfo(client, aliasName);\n\n const actions: Record<string, unknown>[] = [{ add: { index: newIndex, alias: aliasName } }];\n if (aliasInfo) {\n // Remove the alias from whatever index it currently points to.\n actions.push({ remove: { index: aliasInfo.index, alias: aliasName } });\n }\n\n await client.indices.updateAliases({ body: { actions } });\n}\n\nasync function getCurrentAliasInfo(client: Client, aliasName: string): Promise<{ alias: string; index: string } | null> {\n try {\n const exists = await client.indices.existsAlias({ name: aliasName });\n // SDK 3.x: existsAlias returns `{ body: boolean, statusCode, ... }`.\n // Treat anything that doesn't unwrap to `true` as \"not present\".\n if (!exists.body) return null;\n } catch {\n return null;\n }\n const aliases = await client.cat.aliases({ name: aliasName, format: 'json' });\n const list = (aliases.body ?? []) as unknown as Array<{ alias: string; index: string }>;\n return list.length > 0 ? { alias: list[0].alias, index: list[0].index } : null;\n}\n\nasync function deleteOldIndices(client: Client, baseIndexName: string, keepIndexName: string): Promise<void> {\n // Server-side prefix filter so we don't pull every index in the\n // cluster — a Crowi cluster can be shared with other apps.\n const indices = await client.cat.indices({ index: `${baseIndexName}-*`, format: 'json' });\n const list = (indices.body ?? []) as unknown as Array<{ index: string }>;\n // Belt-and-braces: also enforce the timestamp format on the client\n // side. Anything matching `<base>-*` but lacking the 17-digit\n // timestamp + 4-char random suffix is left alone (could be a\n // hand-created index named `<base>-staging` etc.).\n const toDelete = list.map((i) => i.index).filter((name) => name.startsWith(`${baseIndexName}-`) && name !== keepIndexName && TS_INDEX_RE.test(name));\n if (toDelete.length === 0) return;\n await client.indices.delete({ index: toDelete });\n}\n\nfunction isNotFoundError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as { statusCode?: number; meta?: { statusCode?: number } };\n return e.statusCode === 404 || e.meta?.statusCode === 404;\n}\n\n// ---------------------------------------------------------------------------\n// Document shape conversion\n// ---------------------------------------------------------------------------\n\n/**\n * Single source of truth for the search-doc field names. We keep the\n * `ES_FIELDS` identifier verbatim from the ES plugin: these field\n * names go on the wire to OpenSearch but they are also identical to\n * what the ES plugin writes, by design — a re-point + rebuild between\n * the two backends needs no mapping rewrite. Renaming the constant\n * would obscure that invariant.\n */\nexport const ES_FIELDS = {\n path: 'path',\n body: 'body',\n username: 'username',\n grant: 'grant',\n grantedUsers: 'granted_users',\n commentCount: 'comment_count',\n bookmarkCount: 'bookmark_count',\n likeCount: 'like_count',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n} as const;\n\ninterface OsPageSource {\n path: string;\n body: string;\n username?: string;\n grant?: number;\n granted_users?: string[];\n comment_count?: number;\n bookmark_count?: number;\n like_count?: number;\n created_at?: Date | string;\n updated_at?: Date | string;\n}\n\n/**\n * SearchableDoc -> OpenSearch page source. Plugin-API only mandates\n * id / path / body; everything else we look up in `meta.*` for the\n * keys the legacy indexer produced. Unknown / missing keys are simply\n * omitted (mapping is non-strict, so OpenSearch tolerates that).\n */\nexport function docToEsSource(doc: SearchableDoc): OsPageSource {\n const meta = (doc.meta ?? {}) as Record<string, unknown>;\n const source: OsPageSource = {\n path: doc.path,\n body: doc.body,\n };\n const username = readString(meta.username);\n if (username !== undefined) source.username = username;\n const grant = readNumber(meta.grant);\n if (grant !== undefined) source.grant = grant;\n const grantedUsers = readStringArray(meta.granted_users ?? meta.grantedUsers);\n if (grantedUsers !== undefined) source.granted_users = grantedUsers;\n const commentCount = readNumber(meta.comment_count ?? meta.commentCount);\n if (commentCount !== undefined) source.comment_count = commentCount;\n const bookmarkCount = readNumber(meta.bookmark_count ?? meta.bookmarkCount);\n if (bookmarkCount !== undefined) source.bookmark_count = bookmarkCount;\n const likeCount = readNumber(meta.like_count ?? meta.likeCount);\n if (likeCount !== undefined) source.like_count = likeCount;\n const createdAt = readDateLike(meta.created_at ?? meta.createdAt);\n if (createdAt !== undefined) source.created_at = createdAt;\n const updatedAt = readDateLike(meta.updated_at ?? meta.updatedAt);\n if (updatedAt !== undefined) source.updated_at = updatedAt;\n return source;\n}\n\n/**\n * Project a `PageStreamDoc` (Mongo lean shape) into a `SearchableDoc`\n * and route through the canonical `docToEsSource`. Centralises\n * the meta key vocabulary so we have one place to evolve.\n */\nfunction pageStreamDocToEsSource(doc: PageStreamDoc, bookmarkCount: number): OsPageSource {\n const grantedUsers = (doc.grantedUsers ?? []).map((u) => (typeof u === 'string' ? u : u.toString()));\n const searchable: SearchableDoc = {\n id: typeof doc._id === 'string' ? doc._id : doc._id.toString(),\n path: doc.path,\n body: doc.revision?.body ?? '',\n meta: {\n username: doc.creator?.username,\n grant: doc.grant,\n granted_users: grantedUsers,\n comment_count: doc.commentCount ?? 0,\n bookmark_count: bookmarkCount,\n like_count: doc.liker?.length ?? 0,\n created_at: doc.createdAt,\n updated_at: doc.updatedAt,\n },\n };\n return docToEsSource(searchable);\n}\n\ninterface OsHit {\n _id: unknown;\n _score?: number;\n _source?: unknown;\n highlight?: Record<string, string[]>;\n}\n\ninterface OsSearchResponseBody {\n hits?: {\n total?: number | { value: number };\n hits?: OsHit[];\n };\n}\n\nfunction pickSnippet(highlight: Record<string, string[]> | undefined): string | undefined {\n if (!highlight) return undefined;\n // Prefer Japanese body, then English body, then path. Pick the first\n // fragment for each — OpenSearch sorts within a field by score so\n // the first is the most relevant.\n for (const field of ['body.ja', 'body', 'path.ja', 'body.en', 'path.en']) {\n const fragments = highlight[field];\n if (fragments && fragments.length > 0) return fragments[0];\n }\n return undefined;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction readNumber(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n return undefined;\n}\n\nfunction readStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const out: string[] = [];\n for (const v of value) {\n if (typeof v === 'string') out.push(v);\n else if (v && typeof v === 'object' && typeof (v as { toString?: () => string }).toString === 'function') {\n out.push((v as { toString: () => string }).toString());\n }\n }\n return out;\n}\n\nfunction readDateLike(value: unknown): Date | string | undefined {\n if (value instanceof Date) return value;\n if (typeof value === 'string') return value;\n return undefined;\n}\n","/**\n * Search-string parser for the OpenSearch driver.\n *\n * Splits a free-form query into positive / negative keywords and\n * phrases. Lifted from the legacy `packages/api/src/service/query.ts`\n * with no behaviour changes — preserved here as a plugin-private\n * helper because the parser is currently OpenSearch / Elasticsearch\n * specific (the +/- and `\"phrase\"` syntax maps directly to\n * `multi_match` queries). When a future driver wants the same shape,\n * factor it back into `@crowi/plugin-api`.\n */\n\nexport type PositiveAndNegative<T> = {\n positive: T;\n negative: T;\n};\n\nexport type ParsedSearchQuery = {\n keywords: PositiveAndNegative<string[]>;\n phrases: PositiveAndNegative<string[]>;\n};\n\nexport const normalize = (query: string): string => {\n return query.trim().replace(/\\s+/g, ' ');\n};\n\nexport const splitKeywordsAndPhrases = (query: string): { keywords: string[]; phrases: string[] } => {\n const phraseRegExp = /(-?\"[^\"]*\")/g;\n const keywords = query.replace(phraseRegExp, '').split(/\\s+/g).filter(Boolean);\n const phrases = (query.match(phraseRegExp) || []).map(normalize);\n return { keywords, phrases };\n};\n\nexport const splitPositiveAndNegative = (queries: string[]): PositiveAndNegative<string[]> => {\n const positive: string[] = [];\n const negative: string[] = [];\n for (const query of queries) {\n const isNegative = query.startsWith('-');\n const target = isNegative ? negative : positive;\n const newQuery = isNegative ? query.substring(1) : query;\n\n if (newQuery) {\n target.push(newQuery);\n }\n }\n return { positive, negative };\n};\n\n/**\n * Strip the surrounding `\"` quotes from a phrase token. Negative\n * markers (`-`) are stripped earlier by `splitPositiveAndNegative`,\n * so by the time we get here the input is always `\"…\"`.\n */\nexport const unquote = (query: string): string => {\n return query.slice(1, -1);\n};\n\nexport const parseQuery = (query: string): ParsedSearchQuery => {\n const { keywords, phrases } = splitKeywordsAndPhrases(normalize(query));\n const { positive: positiveKeywords, negative: negativeKeywords } = splitPositiveAndNegative(keywords);\n const { positive: positivePhrases, negative: negativePhrases } = splitPositiveAndNegative(phrases);\n\n return {\n keywords: {\n positive: positiveKeywords,\n negative: negativeKeywords,\n },\n phrases: {\n positive: positivePhrases.map(unquote).filter(Boolean),\n negative: negativePhrases.map(unquote).filter(Boolean),\n },\n };\n};\n","/**\n * Build an OpenSearch search request body from the SearchQuery shape\n * exposed by `@crowi/plugin-api`. The driver passes a parsed\n * keyword/phrase tree plus the viewer + grants from the original\n * SearchQuery; this module composes them into a single bool query.\n *\n * The wire shape is identical to Elasticsearch's query DSL (OpenSearch\n * forked from ES 7.10.2 and has kept the search API surface compatible),\n * so the builder is a 1:1 copy of `@crowi/plugin-search-elasticsearch`'s\n * builder for now. Kept private here rather than shared because the two\n * drivers may diverge in the future (e.g. OpenSearch's neural / k-NN\n * extensions) and hard-coupling them would block that.\n *\n * Design notes:\n * - All filters are composed at the top-level `bool`. We never nest\n * a second `bool` for the same operator type (must / filter /\n * should / must_not), so the generated body is small and easy to\n * diff in tests.\n * - The grant filter mirrors the legacy ES Searcher precisely:\n * a non-public page (RESTRICTED / SPECIFIED / OWNER) is hidden\n * unless its `username` field matches the viewer's username.\n * For SPECIFIED / OWNER / RESTRICTED pages, we additionally allow\n * the page through if `granted_users` contains the viewer id —\n * the legacy query only checked `username`, but the new\n * SearchableDoc lets us index `granted_users` precisely so we\n * can express \"shared with me\" as well.\n * - Type filter (portal / public / user) reproduces the legacy\n * `path.raw` regex / prefix queries.\n */\n\nimport type { SearchPageType, SearchQueryGrants, SearchQueryViewer } from '@crowi/plugin-api';\nimport type { ParsedSearchQuery } from './parse-query';\n\n// Page grant constants — mirror `packages/api/src/models/page.ts`.\n// Hard-coded here because the plugin must not import from @crowi/api\n// (that would invert the dependency direction and force a runner\n// rebuild on every plugin change).\nexport const GRANT_PUBLIC = 1;\nexport const GRANT_RESTRICTED = 2;\nexport const GRANT_SPECIFIED = 3;\nexport const GRANT_OWNER = 4;\n\n// TODO: By user's i18n setting, change boost or search target fields.\nexport const defaultKeywordQueryFields = ['path.ja^2', 'body.ja', 'path.en^1.2', 'body.en'];\n// Not use \"*.ja\" fields here because we want to analyse (parse) phrase\n// search words.\nexport const defaultPhraseQueryFields = ['path.raw^2', 'body'];\n\nconst portalQuery = { regexp: { 'path.raw': '.*/' } };\nconst userPathQuery = { prefix: { 'path.raw': '/user/' } };\n\nexport type FunctionScoreParams = {\n fieldValueFactor: {\n field: string;\n factor?: number;\n modifier?: 'log' | 'log1p' | 'log2p' | 'ln' | 'ln1p' | 'ln2p' | 'square' | 'sqrt' | 'reciprocal' | 'none';\n missing: number;\n };\n boostMode: 'multiply' | 'replace' | 'sum' | 'avg' | 'max' | 'min';\n};\n\nexport interface BuildSearchBodyParams {\n parsed: ParsedSearchQuery;\n pathPrefix?: string;\n viewer?: SearchQueryViewer;\n grants?: SearchQueryGrants;\n functionScore?: FunctionScoreParams;\n from: number;\n size: number;\n}\n\n// Loose typing: the OpenSearch SDK accepts plain JSON for the request\n// body, so we keep this internal type representation close to the wire\n// format. Using `unknown` keeps the shape opaque; tests rely on\n// snapshot equality rather than structural typing.\ntype OsQueryBody = Record<string, unknown>;\n\ninterface BoolBuckets {\n must: OsQueryBody[];\n filter: OsQueryBody[];\n should: OsQueryBody[];\n must_not: OsQueryBody[];\n}\n\nconst emptyBuckets = (): BoolBuckets => ({ must: [], filter: [], should: [], must_not: [] });\n\nconst appendKeywords = (buckets: BoolBuckets, keywords: string[], operator: 'and' | 'or', kind: 'must' | 'must_not'): void => {\n if (keywords.length === 0) return;\n buckets[kind].push({\n multi_match: {\n query: keywords.join(' '),\n fields: defaultKeywordQueryFields,\n operator,\n },\n });\n};\n\nconst appendPhrases = (buckets: BoolBuckets, phrases: string[], operator: 'and' | 'or', kind: 'must' | 'must_not'): void => {\n for (const phrase of phrases) {\n buckets[kind].push({\n multi_match: {\n type: 'phrase',\n query: phrase,\n fields: defaultPhraseQueryFields,\n operator,\n },\n });\n }\n};\n\nconst appendTypeFilter = (buckets: BoolBuckets, type: SearchPageType): void => {\n switch (type) {\n case 'portal':\n buckets.must_not.push(userPathQuery);\n buckets.filter.push(portalQuery);\n return;\n case 'public':\n buckets.must_not.push(userPathQuery);\n buckets.must_not.push(portalQuery);\n return;\n case 'user':\n buckets.filter.push(userPathQuery);\n return;\n }\n};\n\nconst appendPathPrefix = (buckets: BoolBuckets, pathPrefix: string): void => {\n const trimmed = pathPrefix.endsWith('/') ? pathPrefix.slice(0, -1) : pathPrefix;\n buckets.filter.push({\n wildcard: {\n 'path.raw': `${trimmed}/*`,\n },\n });\n};\n\nconst appendGrantFilter = (buckets: BoolBuckets, viewer?: SearchQueryViewer): void => {\n if (!viewer) {\n // Anonymous viewer: only public pages.\n buckets.filter.push({ match: { grant: GRANT_PUBLIC } });\n return;\n }\n if (viewer.isAdmin) {\n // Admins see everything; no grant filter.\n return;\n }\n\n // Non-admin authenticated viewer: a page is visible iff\n // grant === GRANT_PUBLIC, OR\n // page.username === viewer.username (legacy semantics — the\n // creator can always find their own restricted/owner pages), OR\n // page.granted_users contains viewer.id (specified/restricted\n // share targets).\n buckets.filter.push({\n bool: {\n should: [{ term: { grant: GRANT_PUBLIC } }, { term: { username: viewer.username } }, { term: { granted_users: viewer.id } }],\n minimum_should_match: 1,\n },\n });\n};\n\n/**\n * Build the OpenSearch search request body. Returns an object suitable\n * for `client.search({ index, body })`.\n */\nexport function buildSearchBody(params: BuildSearchBodyParams): {\n from: number;\n size: number;\n sort: Array<Record<string, unknown>>;\n highlight: Record<string, unknown>;\n query: Record<string, unknown>;\n _source: string[];\n} {\n const { parsed, pathPrefix, viewer, grants, functionScore, from, size } = params;\n const buckets = emptyBuckets();\n\n appendKeywords(buckets, parsed.keywords.positive, 'and', 'must');\n appendKeywords(buckets, parsed.keywords.negative, 'or', 'must_not');\n appendPhrases(buckets, parsed.phrases.positive, 'and', 'must');\n appendPhrases(buckets, parsed.phrases.negative, 'or', 'must_not');\n\n if (pathPrefix) {\n appendPathPrefix(buckets, pathPrefix);\n }\n\n if (grants?.types && grants.types.length > 0) {\n // Multiple types are OR-combined as separate `should` clauses.\n if (grants.types.length === 1) {\n appendTypeFilter(buckets, grants.types[0]);\n } else {\n const typeShoulds: OsQueryBody[] = grants.types.map((t) => {\n const inner = emptyBuckets();\n appendTypeFilter(inner, t);\n return { bool: pruneBool(inner) };\n });\n buckets.filter.push({\n bool: { should: typeShoulds, minimum_should_match: 1 },\n });\n }\n }\n\n appendGrantFilter(buckets, viewer);\n\n const baseQuery: Record<string, unknown> = { bool: pruneBool(buckets) };\n\n const query = functionScore\n ? {\n function_score: {\n query: baseQuery,\n field_value_factor: functionScore.fieldValueFactor,\n boost_mode: functionScore.boostMode,\n },\n }\n : baseQuery;\n\n return {\n from,\n size,\n sort: [{ _score: 'desc' }],\n highlight: {\n pre_tags: ['<mark>'],\n post_tags: ['</mark>'],\n fields: {\n 'path.ja': {},\n 'body.ja': {},\n body: {},\n },\n },\n query,\n _source: ['path', 'bookmark_count', 'username', 'grant'],\n };\n}\n\n/**\n * Strip empty arrays so the wire body stays compact. OpenSearch\n * accepts an empty bool clause but it pollutes snapshots.\n */\nfunction pruneBool(buckets: BoolBuckets): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (buckets.must.length > 0) out.must = buckets.must;\n if (buckets.filter.length > 0) out.filter = buckets.filter;\n if (buckets.should.length > 0) out.should = buckets.should;\n if (buckets.must_not.length > 0) out.must_not = buckets.must_not;\n return out;\n}\n","{\n \"settings\": {\n \"analysis\": {\n \"filter\": {\n \"english_stop\": {\n \"type\": \"stop\",\n \"stopwords\": \"_english_\"\n },\n \"english_stemmer\": {\n \"type\": \"stemmer\",\n \"language\": \"english\"\n },\n \"english_possessive_stemmer\": {\n \"type\": \"stemmer\",\n \"language\": \"possessive_english\"\n }\n },\n \"tokenizer\": {\n \"ngram_tokenizer\": {\n \"type\": \"ngram\",\n \"min_gram\": 2,\n \"max_gram\": 3,\n \"token_chars\": [\"letter\", \"digit\"]\n }\n },\n \"analyzer\": {\n \"english\": {\n \"tokenizer\": \"ngram_tokenizer\",\n \"filter\": [\"english_possessive_stemmer\", \"lowercase\", \"english_stop\", \"english_stemmer\"]\n }\n }\n }\n },\n \"mappings\": {\n \"properties\": {\n \"path\": {\n \"type\": \"text\",\n \"fields\": {\n \"raw\": {\n \"type\": \"text\",\n \"analyzer\": \"keyword\"\n },\n \"en\": {\n \"type\": \"text\",\n \"analyzer\": \"english\"\n }\n }\n },\n \"body\": {\n \"type\": \"text\",\n \"fields\": {\n \"en\": {\n \"type\": \"text\",\n \"analyzer\": \"english\"\n }\n }\n },\n \"username\": {\n \"type\": \"keyword\"\n },\n \"grant\": {\n \"type\": \"integer\"\n },\n \"granted_users\": {\n \"type\": \"keyword\"\n },\n \"comment_count\": {\n \"type\": \"integer\"\n },\n \"bookmark_count\": {\n \"type\": \"integer\"\n },\n \"like_count\": {\n \"type\": \"integer\"\n },\n \"created_at\": {\n \"type\": \"date\",\n \"format\": \"strict_date_optional_time\"\n },\n \"updated_at\": {\n \"type\": \"date\",\n \"format\": \"strict_date_optional_time\"\n }\n }\n }\n}\n","{\n \"mappings\": {\n \"properties\": {\n \"path\": {\n \"fields\": {\n \"ja\": {\n \"type\": \"text\",\n \"analyzer\": \"kuromoji\"\n }\n }\n },\n \"body\": {\n \"fields\": {\n \"ja\": {\n \"type\": \"text\",\n \"analyzer\": \"kuromoji\"\n }\n }\n }\n }\n }\n}\n","{\n \"settings\": {\n \"analysis\": {\n \"tokenizer\": {\n \"sudachi_tokenizer\": {\n \"type\": \"sudachi_tokenizer\",\n \"mode\": \"search\"\n }\n },\n \"analyzer\": {\n \"sudachi_analyzer\": {\n \"filter\": [],\n \"tokenizer\": \"sudachi_tokenizer\",\n \"type\": \"custom\"\n }\n }\n }\n },\n \"mappings\": {\n \"properties\": {\n \"path\": {\n \"fields\": {\n \"ja\": {\n \"type\": \"text\",\n \"analyzer\": \"sudachi_analyzer\"\n }\n }\n },\n \"body\": {\n \"fields\": {\n \"ja\": {\n \"type\": \"text\",\n \"analyzer\": \"sudachi_analyzer\"\n }\n }\n }\n }\n }\n}\n"],"mappings":"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","names":["state"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crowi/plugin-search-opensearch",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "OpenSearch search driver for Crowi 2.0.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"zod": "^4.4.3"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@opensearch-project/opensearch": "^3.6.0",
|
|
27
|
+
"@crowi/plugin-api": "^0.1.0-alpha.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/jest": "^29.5.14",
|
|
31
|
+
"@types/node": "^24",
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"ts-jest": "^29.3.4",
|
|
34
|
+
"tsup": "^8.3.5",
|
|
35
|
+
"typescript": "^5.8.3",
|
|
36
|
+
"zod": "^4.4.3",
|
|
37
|
+
"@crowi/tsconfig": "0.1.0-alpha.0",
|
|
38
|
+
"@crowi/plugin-api": "0.1.0-alpha.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch --no-clean",
|
|
43
|
+
"type-check": "tsc --noEmit",
|
|
44
|
+
"test": "jest"
|
|
45
|
+
}
|
|
46
|
+
}
|