@crowi/plugin-search-elasticsearch 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 +105 -0
- package/dist/index.d.mts +250 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.js +831 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +801 -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-elasticsearch — search driver registering\n * `'elasticsearch'` against the SearchRegistry.\n *\n * Activation: add this plugin to the runner's `crowi.config.json`\n * `plugins` array and set `search.driver: 'elasticsearch'`. Configure\n * via the Mongo Config namespace `plugin:@crowi/plugin-search-elasticsearch:*`\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 createElasticsearchDriver,\n type Analyzer,\n type ElasticsearchDriver,\n type ElasticsearchDriverConfig,\n type ESDriverState,\n type PageStreamDoc,\n} from './driver';\n\nexport { applyConfig, applyConfigInPlace, createElasticsearchDriver } from './driver';\nexport type { ElasticsearchDriver, ElasticsearchDriverConfig, ElasticsearchDriverDeps, ESDriverState, PageStreamDoc, Analyzer } from './driver';\nexport { parseQuery } from './parse-query';\nexport { buildSearchBody } from './query-builder';\n\nexport const ElasticsearchConfigSchema = 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 Elasticsearch 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 ES plugin.\n * - `kuromoji`: `analysis-kuromoji` plugin (Elastic-distributed).\n * The dev image (`elasticsearch.Dockerfile`) preinstalls it.\n * - `sudachi`: third-party `analysis-sudachi` plugin + dictionary.\n * NOT bundled in the dev image; operators must build a derived\n * image. Picking this without the plugin makes `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 ElasticsearchConfig = z.infer<typeof ElasticsearchConfigSchema>;\n\nconst PLUGIN_NAME = '@crowi/plugin-search-elasticsearch';\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 `'elasticsearch'` driver, owned by this\n * module. `null` before `registerSearch` runs.\n */\nlet state: ESDriverState | null = null;\n\nfunction toDriverConfig(config: ElasticsearchConfig): ElasticsearchDriverConfig {\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: ElasticsearchConfigSchema,\n adminPlacement: {\n label: 'Elasticsearch',\n icon: 'search',\n // section omitted: derived from registerSearch -> 'search'\n },\n\n registerSearch: (registry, ctx) => {\n const config = ctx.config<ElasticsearchConfig>();\n\n if (!config.url) {\n ctx.log.warn('url is empty; the elasticsearch 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('elasticsearch', driver);\n ctx.log.debug('registered elasticsearch 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 Elasticsearch search.');\n return;\n }\n const config = ctx.config<ElasticsearchConfig>();\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 ES Client keeps an HTTP keep-alive pool, so\n // the old one must be closed — but awaiting it would block the\n // admin save response. Inflight requests already snapshotted the\n // 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 Elasticsearch client failed: %o', err);\n });\n }\n ctx.log.debug('reconfigured elasticsearch 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: ESDriverState, ctx: PluginContext): ElasticsearchDriver {\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 createElasticsearchDriver(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>. Replaces the\n // legacy per-doc `Bookmark.countByPageId` lookup that was\n // O(N) round-trips 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 * Elasticsearch 9 driver implementing the `SearchDriver` contract.\n * Owns the Client, the `${indexName}-current` alias (legacy ops\n * compat), single-doc index / remove, query against the alias, and\n * rebuild-from-scratch in 2k-doc bulk batches with bookmark counts\n * pre-fetched in one aggregate. Document field shape (path / body /\n * username / grant / granted_users / *_count / *_at) matches the\n * legacy ES7 indexer for reindex-free migration.\n */\n\nimport { Client } from '@elastic/elasticsearch';\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 ElasticsearchDriverConfig {\n url: string;\n indexName: string;\n requestTimeout: number;\n analyzer: Analyzer;\n}\n\nexport interface ElasticsearchDriverDeps {\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 ElasticsearchDriver extends SearchDriver {\n /** Currently-targeted alias name (`<indexName>-current`). Exposed for tests / admin UI. */\n readonly aliasName: string;\n /** ES 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. `createElasticsearchDriver` 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 ESDriverState {\n /** `null` when `url` is empty (driver configured-but-disabled). */\n client: Client | null;\n /** ES 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 ESDriverState} 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: ElasticsearchDriverConfig): ESDriverState {\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: ESDriverState, config: ElasticsearchDriverConfig): { oldClient: Client | null } {\n const oldClient = target.client;\n // Assign over the freshly-built state so a new ESDriverState 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 a {@link ESDriverState} 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 createElasticsearchDriver(state: ESDriverState, deps: ElasticsearchDriverDeps = {}): ElasticsearchDriver {\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: ElasticsearchDriver = {\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 document: 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. ES9 throws a\n // `ResponseError` with statusCode 404 when the id doesn't\n // 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 // ES9 client narrows SearchRequest types (esp. `highlight.fields`)\n // beyond what `buildSearchBody` returns; the runtime shape is correct,\n // so cast through `unknown` to the SearchRequest shape.\n const response = await client.search({\n index: aliasName,\n ...body,\n } as unknown as Parameters<Client['search']>[0]);\n\n const totalRaw = response.hits?.total;\n const total = typeof totalRaw === 'number' ? totalRaw : (totalRaw?.value ?? 0);\n\n const rawHits = (response.hits?.hits ?? []) as unknown as EsHit[];\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-elasticsearch: 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, ...mapping });\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 Elasticsearch\" error rather than a `TypeError` on `null`.\n */\nconst SEARCH_NOT_CONFIGURED = '@crowi/plugin-search-elasticsearch: Search not configured (Elasticsearch 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: ESDriverState): { 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 Elasticsearch should starts with http/https');\n }\n\n const esUrl = new URL(uri);\n const auth = esUrl.username && esUrl.password ? `${esUrl.username}:${esUrl.password}@` : '';\n const node = `${esUrl.protocol}//${auth}${esUrl.host}`;\n const indexName = esUrl.pathname && esUrl.pathname !== '/' ? esUrl.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 ES\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<ElasticsearchDriverDeps['iteratePages']>;\n countAllPages: NonNullable<ElasticsearchDriverDeps['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 const response = await ctx.client.bulk({\n operations,\n timeout: '1d',\n });\n if (response.errors) {\n ctx.log?.warn('rebuild: bulk had item-level errors (took=%dms)', response.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({ 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 if (!exists) return null;\n } catch {\n return null;\n }\n const aliases = await client.cat.aliases({ name: aliasName, format: 'json' });\n const list = aliases 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 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 ES doc field names. `query-builder.ts`\n * uses these too where applicable; bringing them onto one constant\n * makes a future rename / extension a single-file change.\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 EsPageSource {\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 -> ES page source. Plugin-API only mandates id / path\n * / body; everything else we look up in `meta.*` for the keys the\n * legacy indexer produced. Unknown / missing keys are simply omitted\n * (mapping is non-strict, so ES tolerates that).\n */\nexport function docToEsSource(doc: SearchableDoc): EsPageSource {\n const meta = (doc.meta ?? {}) as Record<string, unknown>;\n const source: EsPageSource = {\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): EsPageSource {\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 EsHit {\n _id: unknown;\n _score?: number;\n _source?: unknown;\n highlight?: Record<string, string[]>;\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 — ES sorts within a field by score so the first\n // 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 Elasticsearch 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 ES-specific (the +/- and\n * `\"phrase\"` syntax maps directly to ES `multi_match` queries). When\n * a future driver wants the same shape, factor it back into\n * `@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 Elasticsearch 8 search request body from the SearchQuery\n * shape 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 * 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: ES9 SDK accepts plain JSON for the request body, so we\n// keep this internal type representation close to the wire format.\n// Using `unknown` keeps the shape opaque; tests rely on snapshot\n// equality rather than structural typing.\ntype EsQueryBody = Record<string, unknown>;\n\ninterface BoolBuckets {\n must: EsQueryBody[];\n filter: EsQueryBody[];\n should: EsQueryBody[];\n must_not: EsQueryBody[];\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 ES9 search request body. Returns an object suitable for\n * `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: EsQueryBody[] = 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. ES accepts an\n * 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"]}
|