@swirl-search/backstage-plugin-search-backend-module-swirl 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SwirlSearchEngine.cjs.js","sources":["../../src/engines/SwirlSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2026 SWIRL AI Connect\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { Writable } from 'node:stream';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport {\n QueryRequestOptions,\n SearchEngine,\n} from '@backstage/plugin-search-backend-node';\nimport {\n IndexableResult,\n IndexableResultSet,\n SearchQuery,\n} from '@backstage/plugin-search-common';\nimport { SwirlClient, SwirlRequestResult } from './SwirlClient';\nimport { SwirlIndexer } from './SwirlIndexer';\nimport { SwirlNoopIndexer } from './SwirlNoopIndexer';\nimport {\n MISSING_INDEX_ERROR_NAME,\n SWIRL_FEDERATED_TYPE,\n SWIRL_HIGHLIGHT_END_MARKER,\n SWIRL_HIGHLIGHT_START_MARKER,\n SWIRL_INDEX_PROVIDER_TAG,\n SwirlEngineConfig,\n SwirlPageCursor,\n SwirlResponse,\n SwirlResult,\n swirlResultScore,\n} from './types';\n\n/**\n * The SWIRL query the engine is about to run, after translation.\n *\n * @public\n */\nexport type ConcreteSwirlQuery = {\n term: string;\n /** Backstage document types read from the SWIRL index. */\n indexTypes?: string[];\n /** Whether the federated providers take part in this query. */\n federated: boolean;\n /** Field filters, forwarded to SWIRL as JSON. */\n filters: Record<string, unknown>;\n /** Results per page. */\n pageSize: number;\n /** Decoded page cursor, absent on page 0. */\n cursor?: SwirlPageCursor;\n};\n\n/**\n * Options handed to a SWIRL query translator.\n *\n * @public\n */\nexport type SwirlQueryTranslatorOptions = {\n federatedEnabled: boolean;\n};\n\n/**\n * SWIRL specific query translator.\n *\n * @public\n */\nexport type SwirlQueryTranslator = (\n query: SearchQuery,\n options: SwirlQueryTranslatorOptions,\n) => ConcreteSwirlQuery;\n\n/**\n * Options to instantiate {@link SwirlSearchEngine}.\n *\n * @public\n */\nexport type SwirlSearchEngineOptions = {\n logger: LoggerService;\n auth: AuthService;\n /** Injectable for tests. Defaults to the global fetch. */\n fetchImpl?: typeof fetch;\n};\n\n/**\n * A Backstage search engine backed by SWIRL. Indexed Backstage documents are\n * served from the SWIRL index; results from connected sources arrive in the\n * same response under the `swirl-federated` type.\n *\n * @public\n */\nexport class SwirlSearchEngine implements SearchEngine {\n private readonly options: SwirlEngineConfig;\n private readonly logger: LoggerService;\n private readonly client: SwirlClient;\n private readonly preTag: string;\n private readonly postTag: string;\n\n private constructor(\n options: SwirlEngineConfig,\n deps: SwirlSearchEngineOptions,\n ) {\n this.options = options;\n this.logger = deps.logger;\n this.client = new SwirlClient({\n baseUrl: options.baseUrl,\n auth: deps.auth,\n audience: options.audience,\n timeoutMs: options.queryTimeoutMs,\n fetchImpl: deps.fetchImpl,\n });\n\n const tag = randomUUID();\n this.preTag = `<${tag}>`;\n this.postTag = `</${tag}>`;\n }\n\n static async fromConfig(\n config: Config,\n deps: SwirlSearchEngineOptions,\n ): Promise<SwirlSearchEngine> {\n const engine = new SwirlSearchEngine(readSwirlConfig(config), deps);\n await engine.pushTuning();\n return engine;\n }\n\n /**\n * Mirrors the app-config tuning block to SWIRL so that relevance is\n * configured in one place. A SWIRL that is not up yet, or an older SWIRL\n * that does not know the endpoint, must not stop the backend from booting.\n *\n * SWIRL answers with the effective tuning in its own flat form plus\n * `accepted_keys`, naming every key it took in the shape it was sent, and a\n * `bm25` notice when it stored BM25 parameters it cannot apply. Both are\n * logged, because a tuning block that is accepted by Backstage and then\n * quietly dropped by SWIRL is exactly the failure this call exists to make\n * visible. A 400 names the keys SWIRL did not recognise; that is a warning,\n * not a boot failure.\n */\n private async pushTuning(): Promise<void> {\n try {\n const token = await this.client.mintToken();\n const result = await this.client.request({\n url: this.client.url('/swirl/index/config/'),\n method: 'POST',\n token,\n body: this.options.tuning,\n });\n\n if (!result.ok) {\n const rejected = rejectedTuningKeys(result.body);\n const detail = rejected.length\n ? ` SWIRL did not recognise: ${rejected.join(', ')}.`\n : describeTuningError(result.body);\n this.logger.warn(\n `SWIRL rejected the relevance tuning block: HTTP ${result.status}.${detail} SWIRL keeps its current tuning.`,\n );\n return;\n }\n\n const body = (result.body ?? {}) as {\n accepted_keys?: unknown;\n bm25?: unknown;\n };\n const accepted = Array.isArray(body.accepted_keys)\n ? body.accepted_keys.map(String)\n : [];\n\n this.logger.info(\n accepted.length\n ? `Mirrored the relevance tuning block to SWIRL; SWIRL accepted: ${accepted.join(\n ', ',\n )}`\n : 'Mirrored the relevance tuning block to SWIRL; SWIRL reported no accepted tuning keys',\n );\n\n if (typeof body.bm25 === 'string' && body.bm25) {\n this.logger.warn(\n `SWIRL stored the bm25 tuning values but reports them \"${body.bm25}\", so search.swirl.tuning.bm25 has no effect on ranking.`,\n );\n }\n } catch (e) {\n this.logger.warn(\n `Could not send the relevance tuning block to SWIRL at ${this.options.baseUrl}: ${e}. SWIRL keeps its current tuning.`,\n );\n }\n }\n\n translator(\n query: SearchQuery,\n options: SwirlQueryTranslatorOptions,\n ): ConcreteSwirlQuery {\n const pageSize = query.pageLimit || 25;\n const cursor = decodePageCursor(query.pageCursor);\n\n // The federated lane runs when the caller did not narrow by type, or\n // asked for the federated type by name. Under permissions the router\n // always passes the full list of registered types, which is why the\n // federated type has to be registered at all.\n const federated =\n options.federatedEnabled &&\n (query.types === undefined || query.types.includes(SWIRL_FEDERATED_TYPE));\n\n const indexTypes = query.types?.filter(\n type => type !== SWIRL_FEDERATED_TYPE,\n );\n\n return {\n term: query.term ?? '',\n indexTypes,\n federated,\n filters: (query.filters as Record<string, unknown>) ?? {},\n pageSize,\n cursor,\n };\n }\n\n setTranslator(translator: SwirlQueryTranslator) {\n this.translator = translator;\n }\n\n async getIndexer(type: string): Promise<Writable> {\n if (type === SWIRL_FEDERATED_TYPE) {\n return new SwirlNoopIndexer({ type, logger: this.logger });\n }\n\n return new SwirlIndexer({\n type,\n batchSize: this.options.indexerBatchSize,\n client: this.client,\n logger: this.logger,\n });\n }\n\n async query(\n query: SearchQuery,\n options?: QueryRequestOptions,\n ): Promise<IndexableResultSet> {\n const concrete = this.translator(query, {\n federatedEnabled: this.options.federated.enabled,\n });\n\n const token = await this.resolveToken(options);\n const result = concrete.cursor\n ? await this.fetchResultPage(concrete, concrete.cursor, token)\n : await this.fetchFirstPage(concrete, token);\n\n this.assertIndexPresent(result);\n\n if (!result.ok) {\n throw new Error(\n `SWIRL returned HTTP ${result.status} for the query ${JSON.stringify(\n concrete.term,\n )}`,\n );\n }\n\n const body = (result.body ?? {}) as SwirlResponse;\n const page = concrete.cursor?.p ?? 0;\n const searchId = concrete.cursor?.s ?? body.info?.search?.id;\n const swirlResults = body.results ?? [];\n\n const results = swirlResults.map((entry, index) =>\n this.toIndexableResult(entry, page * concrete.pageSize + index + 1),\n );\n\n const hasNextPage =\n searchId !== undefined && swirlResults.length >= concrete.pageSize;\n\n return {\n results,\n numberOfResults:\n body.info?.results?.found_total ??\n body.info?.results?.retrieved_total ??\n undefined,\n nextPageCursor: hasNextPage\n ? encodePageCursor({ s: searchId!, p: page + 1 })\n : undefined,\n previousPageCursor:\n page > 0 && searchId !== undefined\n ? encodePageCursor({ s: searchId, p: page - 1 })\n : undefined,\n };\n }\n\n /**\n * Page 0 federates: SWIRL runs the query across the Backstage index and,\n * when the federated lane is active, the connected providers too.\n */\n private async fetchFirstPage(\n concrete: ConcreteSwirlQuery,\n token: string,\n ): Promise<SwirlRequestResult> {\n const providers = [SWIRL_INDEX_PROVIDER_TAG];\n if (concrete.federated) {\n providers.push(...this.options.federated.providerTags);\n }\n\n return this.client.request({\n url: this.client.url('/swirl/search/', {\n qs: concrete.term,\n providers: providers.join(','),\n backstage_types: concrete.indexTypes?.join(',') ?? '',\n backstage_filters: JSON.stringify(concrete.filters),\n backstage_timeout_ms: concrete.federated\n ? this.options.federated.timeoutMs\n : undefined,\n results_requested: concrete.pageSize,\n rag: 'false',\n }),\n method: 'GET',\n token,\n timeoutMs: this.options.queryTimeoutMs,\n });\n }\n\n /**\n * Page N is a database read in SWIRL, not a second federation. That keeps\n * the paging loop in Backstage's AuthorizedSearchEngine cheap.\n */\n private async fetchResultPage(\n concrete: ConcreteSwirlQuery,\n cursor: SwirlPageCursor,\n token: string,\n ): Promise<SwirlRequestResult> {\n return this.client.request({\n url: this.client.url('/swirl/results/', {\n search_id: String(cursor.s),\n page: cursor.p + 1,\n results_requested: concrete.pageSize,\n }),\n method: 'GET',\n token,\n timeoutMs: this.options.queryTimeoutMs,\n });\n }\n\n /**\n * The search router hands the engine a plugin token minted per request,\n * carrying the caller's identity in its `obo` claim; that token is what\n * SWIRL verifies. Programmatic callers that reach the engine directly get\n * a freshly minted one instead.\n */\n private async resolveToken(options?: QueryRequestOptions): Promise<string> {\n if (options && 'token' in options && options.token) {\n return options.token;\n }\n\n const credentials =\n options && 'credentials' in options\n ? (options.credentials as BackstageCredentials)\n : undefined;\n\n return this.client.mintToken(credentials);\n }\n\n /**\n * SWIRL reports a type with no live index either as a 404 with an\n * `missing_index` error body, or as a structured `__MISSING_INDEX__` entry\n * in the response messages. Either way the caller asked for something that\n * has never been indexed, which is worth saying out loud rather than\n * returning an empty result set or a bare 500.\n */\n private assertIndexPresent(result: SwirlRequestResult): void {\n const body = result.body;\n\n if (result.status === 404 && body?.error === 'missing_index') {\n throw missingIndexError(body?.types);\n }\n\n for (const message of body?.messages ?? []) {\n if (\n typeof message !== 'string' ||\n !message.includes('__MISSING_INDEX__')\n ) {\n continue;\n }\n\n // SWIRL banner text and other free form strings share this array, so a\n // message that is not JSON is simply not one of ours.\n let parsed: any;\n try {\n parsed = JSON.parse(message);\n } catch {\n continue;\n }\n\n if (parsed?.type === '__MISSING_INDEX__') {\n throw missingIndexError(parsed.types);\n }\n }\n }\n\n private toIndexableResult(entry: SwirlResult, rank: number): IndexableResult {\n const backstage = entry.payload?.backstage;\n const indexed =\n backstage?.type !== undefined && backstage?.document !== undefined;\n\n return {\n type: indexed ? backstage!.type : SWIRL_FEDERATED_TYPE,\n document: indexed\n ? (backstage!.document as any)\n : {\n // Stripped defensively. SWIRL's relevancy processor writes the\n // marked up text back over `title` and `body`, which is what its\n // own UI renders; a Backstage renderer shows document text as\n // plain text, so the markers arrived on screen as literal\n // `<em>`. Current SWIRL keeps these fields clean, older ones do\n // not, and the engine has to be safe against both.\n title: this.stripMarkers(entry.title),\n text: this.stripMarkers(entry.body),\n location: entry.url ?? '',\n source: entry.searchprovider ?? '',\n // Federated results are not in any Backstage index, so SWIRL's\n // score is the only ranking signal a renderer can show. Indexed\n // documents are handed back exactly as Backstage collated them.\n score: swirlResultScore(entry),\n },\n rank,\n highlight: this.toHighlight(entry),\n };\n }\n\n private toHighlight(entry: SwirlResult) {\n if (!this.options.highlight.enabled) {\n return { preTag: this.preTag, postTag: this.postTag, fields: {} };\n }\n\n // Only the hit highlight lists. A marker sitting in the plain title or\n // body is not a hit - SWIRL keeps its hits in these two lists - and using\n // the plain field here would let a document forge its own highlight.\n const fields: Record<string, string> = {};\n const title = this.rewriteHighlight(entry.title_hit_highlights);\n const text = this.rewriteHighlight(entry.body_hit_highlights);\n\n if (title) {\n fields.title = title;\n }\n if (text) {\n fields.text = text;\n }\n\n return { preTag: this.preTag, postTag: this.postTag, fields };\n }\n\n /** Removes the configured marker pair, leaving the text it wrapped. */\n private stripMarkers(value: string | undefined): string {\n if (!value) {\n return '';\n }\n const { startMarker, endMarker } = this.options.highlight;\n return value.split(startMarker).join('').split(endMarker).join('');\n }\n\n /**\n * SWIRL wraps hits in a configurable marker pair, `<em>` and `</em>` out of\n * the box. Backstage expects the engine's own per-instance tags instead, so\n * that a document body containing the marker cannot forge a highlight.\n *\n * The `maxChars` budget counts visible characters, not tags, and the walk\n * never emits an unbalanced tag: a snippet cut short inside a hit closes it.\n */\n private rewriteHighlight(highlights?: string[]): string | undefined {\n const raw = (highlights ?? []).find(value => Boolean(value));\n if (!raw) {\n return undefined;\n }\n\n const { startMarker, endMarker, maxChars } = this.options.highlight;\n const pattern = new RegExp(\n `${escapeRegExp(startMarker)}([\\\\s\\\\S]*?)${escapeRegExp(endMarker)}`,\n 'g',\n );\n\n let out = '';\n let budget = maxChars;\n let cursor = 0;\n\n const take = (value: string, hit: boolean) => {\n if (budget <= 0 || !value) {\n return;\n }\n const kept = value.slice(0, budget);\n budget -= kept.length;\n out += hit ? `${this.preTag}${kept}${this.postTag}` : kept;\n };\n\n for (const match of raw.matchAll(pattern)) {\n const at = match.index ?? 0;\n take(raw.slice(cursor, at), false);\n take(match[1], true);\n cursor = at + match[0].length;\n }\n take(raw.slice(cursor), false);\n\n return out;\n }\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * The keys SWIRL named in a 400 from POST /swirl/index/config/. SWIRL answers\n * an unrecognised key with a detail line that starts\n * \"unknown tuning key(s): a, b.\" rather than dropping it in silence.\n */\nfunction rejectedTuningKeys(body: any): string[] {\n const detail = typeof body?.detail === 'string' ? body.detail : '';\n const match = detail.match(/unknown tuning key\\(s\\):\\s*(.*)/i);\n if (!match) {\n return [];\n }\n // The detail continues \"... Known keys are ...\", and a nested key such as\n // fuzzy.bogus has a dot in it, so cut on that phrase rather than on a dot.\n return match[1]\n .split(/\\.\\s*Known keys/i)[0]\n .replace(/\\.\\s*$/, '')\n .split(',')\n .map((key: string) => key.trim())\n .filter(Boolean);\n}\n\n/** Whatever SWIRL said about a tuning block it would not take. */\nfunction describeTuningError(body: any): string {\n const detail = typeof body?.detail === 'string' ? body.detail : '';\n return detail ? ` ${detail}` : '';\n}\n\nfunction missingIndexError(types?: unknown): Error {\n const named =\n Array.isArray(types) && types.length ? types.join(', ') : undefined;\n const error = new Error(\n named\n ? `SWIRL has no live index for the requested document type(s): ${named}. Wait for the collator to run, or check the SWIRL ingest logs.`\n : 'SWIRL has no live index for one of the requested document types. Wait for the collator to run, or check the SWIRL ingest logs.',\n );\n error.name = MISSING_INDEX_ERROR_NAME;\n return error;\n}\n\n/** @public */\nexport function decodePageCursor(\n pageCursor?: string,\n): SwirlPageCursor | undefined {\n if (!pageCursor) {\n return undefined;\n }\n\n const decoded = JSON.parse(\n Buffer.from(pageCursor, 'base64').toString('utf-8'),\n );\n if (\n decoded === null ||\n typeof decoded !== 'object' ||\n decoded.s === undefined ||\n typeof decoded.p !== 'number' ||\n decoded.p < 0\n ) {\n throw new Error('Invalid page cursor');\n }\n\n return { s: decoded.s, p: decoded.p };\n}\n\n/** @public */\nexport function encodePageCursor(cursor: SwirlPageCursor): string {\n return Buffer.from(JSON.stringify(cursor), 'utf-8').toString('base64');\n}\n\n/** @public */\nexport function readSwirlConfig(config: Config): SwirlEngineConfig {\n const swirl = config.getConfig('search.swirl');\n const federated = swirl.getOptionalConfig('federated');\n const highlight = swirl.getOptionalConfig('highlight');\n const tuning = swirl.getOptionalConfig('tuning');\n\n return {\n baseUrl: swirl.getString('baseUrl'),\n audience: swirl.getOptionalString('audience') ?? 'search',\n indexerBatchSize: swirl.getOptionalNumber('indexerBatchSize') ?? 500,\n queryTimeoutMs: swirl.getOptionalNumber('queryTimeoutMs') ?? 8000,\n federated: {\n enabled: federated?.getOptionalBoolean('enabled') ?? true,\n providerTags: federated?.getOptionalStringArray('providerTags') ?? [\n 'backstage',\n ],\n timeoutMs: federated?.getOptionalNumber('timeoutMs') ?? 5000,\n },\n tuning: (tuning?.get() as SwirlEngineConfig['tuning']) ?? {},\n highlight: {\n enabled: highlight?.getOptionalBoolean('enabled') ?? true,\n maxChars: highlight?.getOptionalNumber('maxChars') ?? 200,\n startMarker:\n highlight?.getOptionalString('startMarker') ??\n SWIRL_HIGHLIGHT_START_MARKER,\n endMarker:\n highlight?.getOptionalString('endMarker') ?? SWIRL_HIGHLIGHT_END_MARKER,\n },\n };\n}\n"],"names":["SwirlClient","randomUUID","SWIRL_FEDERATED_TYPE","SwirlNoopIndexer","SwirlIndexer","SWIRL_INDEX_PROVIDER_TAG","swirlResultScore","types","MISSING_INDEX_ERROR_NAME","SWIRL_HIGHLIGHT_START_MARKER","SWIRL_HIGHLIGHT_END_MARKER"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const SWIRL_FEDERATED_TYPE = "swirl-federated";
4
+ const SWIRL_INDEX_PROVIDER_TAG = "backstage-index";
5
+ const MISSING_INDEX_ERROR_NAME = "MissingIndexError";
6
+ const SWIRL_HIGHLIGHT_START_MARKER = "<em>";
7
+ const SWIRL_HIGHLIGHT_END_MARKER = "</em>";
8
+ function swirlResultScore(result) {
9
+ return result.payload?.searchprovider_score ?? result.swirl_score;
10
+ }
11
+
12
+ exports.MISSING_INDEX_ERROR_NAME = MISSING_INDEX_ERROR_NAME;
13
+ exports.SWIRL_FEDERATED_TYPE = SWIRL_FEDERATED_TYPE;
14
+ exports.SWIRL_HIGHLIGHT_END_MARKER = SWIRL_HIGHLIGHT_END_MARKER;
15
+ exports.SWIRL_HIGHLIGHT_START_MARKER = SWIRL_HIGHLIGHT_START_MARKER;
16
+ exports.SWIRL_INDEX_PROVIDER_TAG = SWIRL_INDEX_PROVIDER_TAG;
17
+ exports.swirlResultScore = swirlResultScore;
18
+ //# sourceMappingURL=types.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.cjs.js","sources":["../../src/engines/types.ts"],"sourcesContent":["/*\n * Copyright 2026 SWIRL AI Connect\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The document type registered by the federated lane. Results that did not\n * come from the Backstage index are returned under this type.\n *\n * @public\n */\nexport const SWIRL_FEDERATED_TYPE = 'swirl-federated';\n\n/**\n * The SWIRL SearchProvider tag that serves the Backstage index (the Tantivy\n * lane). Always included in the provider list sent to SWIRL.\n *\n * @public\n */\nexport const SWIRL_INDEX_PROVIDER_TAG = 'backstage-index';\n\n/**\n * Name given to the error thrown when SWIRL reports that one of the requested\n * document types has no live index. The search router surfaces the name\n * instead of collapsing it into a generic 500.\n *\n * @public\n */\nexport const MISSING_INDEX_ERROR_NAME = 'MissingIndexError';\n\n/**\n * Options resolved from `search.swirl` in app-config.\n *\n * @public\n */\nexport type SwirlEngineConfig = {\n baseUrl: string;\n audience: string;\n indexerBatchSize: number;\n queryTimeoutMs: number;\n federated: {\n enabled: boolean;\n providerTags: string[];\n timeoutMs: number;\n };\n tuning: SwirlTuning;\n highlight: {\n enabled: boolean;\n maxChars: number;\n startMarker: string;\n endMarker: string;\n };\n};\n\n/**\n * The marker pair SWIRL wraps hits in out of the box, from\n * `SWIRL_HIGHLIGHT_START_CHAR` and `SWIRL_HIGHLIGHT_END_CHAR`.\n *\n * @public\n */\nexport const SWIRL_HIGHLIGHT_START_MARKER = '<em>';\n\n/**\n * @public\n */\nexport const SWIRL_HIGHLIGHT_END_MARKER = '</em>';\n\n/**\n * The relevance tuning block mirrored to SWIRL on startup.\n *\n * @public\n */\nexport type SwirlTuning = {\n fieldBoosts?: { titleExact?: number; titleNgram?: number; text?: number };\n ngram?: { min?: number; max?: number };\n stemmer?: string;\n stopwords?: string[];\n fuzzy?: { enabled?: boolean; distance?: number };\n bm25?: { k1?: number; b?: number };\n};\n\n/**\n * The `payload.backstage` block written by the SWIRL Tantivy connector for\n * documents that came from the Backstage index.\n *\n * @public\n */\nexport type SwirlBackstagePayload = {\n type: string;\n document: Record<string, any>;\n};\n\n/**\n * One entry of the `results` array in a SWIRL response envelope.\n *\n * @public\n */\nexport type SwirlResult = {\n title?: string;\n body?: string;\n url?: string;\n searchprovider?: string;\n swirl_rank?: number;\n swirl_score?: number;\n title_hit_highlights?: string[];\n body_hit_highlights?: string[];\n payload?: Record<string, any> & {\n backstage?: SwirlBackstagePayload;\n /**\n * The provider's own score. SWIRL's MappingResultProcessor sweeps keys it\n * does not recognise off the top level and into the payload, so the\n * Tantivy score arrives here rather than beside `swirl_score`.\n */\n searchprovider_score?: number;\n };\n};\n\n/**\n * Reads the score off a SWIRL result. The provider score lives in the payload\n * because SWIRL moves unrecognised top level keys there; `swirl_score`, which\n * the mixer sets, is the fallback.\n *\n * @public\n */\nexport function swirlResultScore(result: SwirlResult): number | undefined {\n return result.payload?.searchprovider_score ?? result.swirl_score;\n}\n\n/**\n * The SWIRL response envelope returned by `/swirl/search/` and\n * `/swirl/results/`.\n *\n * @public\n */\nexport type SwirlResponse = {\n messages?: string[];\n info?: {\n search?: { id?: number | string };\n results?: {\n found_total?: number;\n retrieved_total?: number;\n next_page?: string;\n prev_page?: string;\n };\n [provider: string]: any;\n };\n results?: SwirlResult[];\n};\n\n/**\n * The cursor the engine hands back to Backstage between pages. Encoded as\n * base64 JSON so that page N is a cheap `/swirl/results/` read rather than a\n * second federation.\n *\n * @public\n */\nexport type SwirlPageCursor = {\n /** SWIRL search id */\n s: number | string;\n /** zero based page number */\n p: number;\n};\n"],"names":[],"mappings":";;AAsBO,MAAM,oBAAA,GAAuB;AAQ7B,MAAM,wBAAA,GAA2B;AASjC,MAAM,wBAAA,GAA2B;AAgCjC,MAAM,4BAAA,GAA+B;AAKrC,MAAM,0BAAA,GAA6B;AA2DnC,SAAS,iBAAiB,MAAA,EAAyC;AACxE,EAAA,OAAO,MAAA,CAAO,OAAA,EAAS,oBAAA,IAAwB,MAAA,CAAO,WAAA;AACxD;;;;;;;"}
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var module$1 = require('./module.cjs.js');
6
+ var types = require('./engines/types.cjs.js');
7
+ var SwirlClient = require('./engines/SwirlClient.cjs.js');
8
+ var SwirlSearchEngine = require('./engines/SwirlSearchEngine.cjs.js');
9
+ var SwirlIndexer = require('./engines/SwirlIndexer.cjs.js');
10
+ var SwirlNoopIndexer = require('./engines/SwirlNoopIndexer.cjs.js');
11
+ var SwirlFederatedCollatorFactory = require('./collators/SwirlFederatedCollatorFactory.cjs.js');
12
+
13
+
14
+
15
+ exports.default = module$1.searchModuleSwirlEngine;
16
+ exports.MISSING_INDEX_ERROR_NAME = types.MISSING_INDEX_ERROR_NAME;
17
+ exports.SWIRL_FEDERATED_TYPE = types.SWIRL_FEDERATED_TYPE;
18
+ exports.SWIRL_HIGHLIGHT_END_MARKER = types.SWIRL_HIGHLIGHT_END_MARKER;
19
+ exports.SWIRL_HIGHLIGHT_START_MARKER = types.SWIRL_HIGHLIGHT_START_MARKER;
20
+ exports.SWIRL_INDEX_PROVIDER_TAG = types.SWIRL_INDEX_PROVIDER_TAG;
21
+ exports.swirlResultScore = types.swirlResultScore;
22
+ exports.SwirlClient = SwirlClient.SwirlClient;
23
+ exports.SwirlSearchEngine = SwirlSearchEngine.SwirlSearchEngine;
24
+ exports.decodePageCursor = SwirlSearchEngine.decodePageCursor;
25
+ exports.encodePageCursor = SwirlSearchEngine.encodePageCursor;
26
+ exports.readSwirlConfig = SwirlSearchEngine.readSwirlConfig;
27
+ exports.SwirlIndexer = SwirlIndexer.SwirlIndexer;
28
+ exports.SwirlNoopIndexer = SwirlNoopIndexer.SwirlNoopIndexer;
29
+ exports.SwirlFederatedCollatorFactory = SwirlFederatedCollatorFactory.SwirlFederatedCollatorFactory;
30
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,455 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import { AuthService, BackstageCredentials, LoggerService } from '@backstage/backend-plugin-api';
3
+ import { Writable, Readable } from 'node:stream';
4
+ import { Config } from '@backstage/config';
5
+ import { SearchEngine, QueryRequestOptions, BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
6
+ import { SearchQuery, IndexableResultSet, IndexableDocument, DocumentCollatorFactory } from '@backstage/plugin-search-common';
7
+
8
+ /**
9
+ * Search backend module for the SWIRL engine.
10
+ *
11
+ * @public
12
+ */
13
+ declare const searchModuleSwirlEngine: _backstage_backend_plugin_api.BackendFeature;
14
+
15
+ /**
16
+ * The document type registered by the federated lane. Results that did not
17
+ * come from the Backstage index are returned under this type.
18
+ *
19
+ * @public
20
+ */
21
+ declare const SWIRL_FEDERATED_TYPE = "swirl-federated";
22
+ /**
23
+ * The SWIRL SearchProvider tag that serves the Backstage index (the Tantivy
24
+ * lane). Always included in the provider list sent to SWIRL.
25
+ *
26
+ * @public
27
+ */
28
+ declare const SWIRL_INDEX_PROVIDER_TAG = "backstage-index";
29
+ /**
30
+ * Name given to the error thrown when SWIRL reports that one of the requested
31
+ * document types has no live index. The search router surfaces the name
32
+ * instead of collapsing it into a generic 500.
33
+ *
34
+ * @public
35
+ */
36
+ declare const MISSING_INDEX_ERROR_NAME = "MissingIndexError";
37
+ /**
38
+ * Options resolved from `search.swirl` in app-config.
39
+ *
40
+ * @public
41
+ */
42
+ type SwirlEngineConfig = {
43
+ baseUrl: string;
44
+ audience: string;
45
+ indexerBatchSize: number;
46
+ queryTimeoutMs: number;
47
+ federated: {
48
+ enabled: boolean;
49
+ providerTags: string[];
50
+ timeoutMs: number;
51
+ };
52
+ tuning: SwirlTuning;
53
+ highlight: {
54
+ enabled: boolean;
55
+ maxChars: number;
56
+ startMarker: string;
57
+ endMarker: string;
58
+ };
59
+ };
60
+ /**
61
+ * The marker pair SWIRL wraps hits in out of the box, from
62
+ * `SWIRL_HIGHLIGHT_START_CHAR` and `SWIRL_HIGHLIGHT_END_CHAR`.
63
+ *
64
+ * @public
65
+ */
66
+ declare const SWIRL_HIGHLIGHT_START_MARKER = "<em>";
67
+ /**
68
+ * @public
69
+ */
70
+ declare const SWIRL_HIGHLIGHT_END_MARKER = "</em>";
71
+ /**
72
+ * The relevance tuning block mirrored to SWIRL on startup.
73
+ *
74
+ * @public
75
+ */
76
+ type SwirlTuning = {
77
+ fieldBoosts?: {
78
+ titleExact?: number;
79
+ titleNgram?: number;
80
+ text?: number;
81
+ };
82
+ ngram?: {
83
+ min?: number;
84
+ max?: number;
85
+ };
86
+ stemmer?: string;
87
+ stopwords?: string[];
88
+ fuzzy?: {
89
+ enabled?: boolean;
90
+ distance?: number;
91
+ };
92
+ bm25?: {
93
+ k1?: number;
94
+ b?: number;
95
+ };
96
+ };
97
+ /**
98
+ * The `payload.backstage` block written by the SWIRL Tantivy connector for
99
+ * documents that came from the Backstage index.
100
+ *
101
+ * @public
102
+ */
103
+ type SwirlBackstagePayload = {
104
+ type: string;
105
+ document: Record<string, any>;
106
+ };
107
+ /**
108
+ * One entry of the `results` array in a SWIRL response envelope.
109
+ *
110
+ * @public
111
+ */
112
+ type SwirlResult = {
113
+ title?: string;
114
+ body?: string;
115
+ url?: string;
116
+ searchprovider?: string;
117
+ swirl_rank?: number;
118
+ swirl_score?: number;
119
+ title_hit_highlights?: string[];
120
+ body_hit_highlights?: string[];
121
+ payload?: Record<string, any> & {
122
+ backstage?: SwirlBackstagePayload;
123
+ /**
124
+ * The provider's own score. SWIRL's MappingResultProcessor sweeps keys it
125
+ * does not recognise off the top level and into the payload, so the
126
+ * Tantivy score arrives here rather than beside `swirl_score`.
127
+ */
128
+ searchprovider_score?: number;
129
+ };
130
+ };
131
+ /**
132
+ * Reads the score off a SWIRL result. The provider score lives in the payload
133
+ * because SWIRL moves unrecognised top level keys there; `swirl_score`, which
134
+ * the mixer sets, is the fallback.
135
+ *
136
+ * @public
137
+ */
138
+ declare function swirlResultScore(result: SwirlResult): number | undefined;
139
+ /**
140
+ * The SWIRL response envelope returned by `/swirl/search/` and
141
+ * `/swirl/results/`.
142
+ *
143
+ * @public
144
+ */
145
+ type SwirlResponse = {
146
+ messages?: string[];
147
+ info?: {
148
+ search?: {
149
+ id?: number | string;
150
+ };
151
+ results?: {
152
+ found_total?: number;
153
+ retrieved_total?: number;
154
+ next_page?: string;
155
+ prev_page?: string;
156
+ };
157
+ [provider: string]: any;
158
+ };
159
+ results?: SwirlResult[];
160
+ };
161
+ /**
162
+ * The cursor the engine hands back to Backstage between pages. Encoded as
163
+ * base64 JSON so that page N is a cheap `/swirl/results/` read rather than a
164
+ * second federation.
165
+ *
166
+ * @public
167
+ */
168
+ type SwirlPageCursor = {
169
+ /** SWIRL search id */
170
+ s: number | string;
171
+ /** zero based page number */
172
+ p: number;
173
+ };
174
+
175
+ /**
176
+ * Outcome of a single call to SWIRL. Non 2xx responses are returned rather
177
+ * than thrown so that callers can decide what a status means; transport
178
+ * failures still throw.
179
+ *
180
+ * @public
181
+ */
182
+ type SwirlRequestResult = {
183
+ status: number;
184
+ ok: boolean;
185
+ body: any;
186
+ };
187
+ /**
188
+ * Options for {@link SwirlClient}.
189
+ *
190
+ * @public
191
+ */
192
+ type SwirlClientOptions = {
193
+ baseUrl: string;
194
+ auth: AuthService;
195
+ /** Plugin id whose JWKS SWIRL trusts. Tokens are minted for this target. */
196
+ audience: string;
197
+ /** Default request timeout in ms. */
198
+ timeoutMs: number;
199
+ /** Injectable for tests. Defaults to the global fetch. */
200
+ fetchImpl?: typeof fetch;
201
+ };
202
+ /**
203
+ * Thin HTTP client for the SWIRL for Backstage service. Owns URL building,
204
+ * bearer handling and timeouts; knows nothing about search semantics.
205
+ *
206
+ * @public
207
+ */
208
+ declare class SwirlClient {
209
+ private readonly baseUrl;
210
+ private readonly auth;
211
+ private readonly audience;
212
+ private readonly timeoutMs;
213
+ private readonly fetchImpl;
214
+ constructor(options: SwirlClientOptions);
215
+ /**
216
+ * Mints a plugin token for SWIRL. Used by the indexer, by the startup
217
+ * tuning call, and by queries that arrive without a router supplied token.
218
+ */
219
+ mintToken(credentials?: BackstageCredentials): Promise<string>;
220
+ /**
221
+ * Builds an absolute URL against the configured base URL. Undefined and
222
+ * null query values are dropped.
223
+ */
224
+ url(path: string, query?: Record<string, string | number | undefined>): string;
225
+ request(options: {
226
+ url: string;
227
+ method: 'GET' | 'POST' | 'DELETE';
228
+ token: string;
229
+ body?: unknown;
230
+ timeoutMs?: number;
231
+ }): Promise<SwirlRequestResult>;
232
+ }
233
+
234
+ /**
235
+ * The SWIRL query the engine is about to run, after translation.
236
+ *
237
+ * @public
238
+ */
239
+ type ConcreteSwirlQuery = {
240
+ term: string;
241
+ /** Backstage document types read from the SWIRL index. */
242
+ indexTypes?: string[];
243
+ /** Whether the federated providers take part in this query. */
244
+ federated: boolean;
245
+ /** Field filters, forwarded to SWIRL as JSON. */
246
+ filters: Record<string, unknown>;
247
+ /** Results per page. */
248
+ pageSize: number;
249
+ /** Decoded page cursor, absent on page 0. */
250
+ cursor?: SwirlPageCursor;
251
+ };
252
+ /**
253
+ * Options handed to a SWIRL query translator.
254
+ *
255
+ * @public
256
+ */
257
+ type SwirlQueryTranslatorOptions = {
258
+ federatedEnabled: boolean;
259
+ };
260
+ /**
261
+ * SWIRL specific query translator.
262
+ *
263
+ * @public
264
+ */
265
+ type SwirlQueryTranslator = (query: SearchQuery, options: SwirlQueryTranslatorOptions) => ConcreteSwirlQuery;
266
+ /**
267
+ * Options to instantiate {@link SwirlSearchEngine}.
268
+ *
269
+ * @public
270
+ */
271
+ type SwirlSearchEngineOptions = {
272
+ logger: LoggerService;
273
+ auth: AuthService;
274
+ /** Injectable for tests. Defaults to the global fetch. */
275
+ fetchImpl?: typeof fetch;
276
+ };
277
+ /**
278
+ * A Backstage search engine backed by SWIRL. Indexed Backstage documents are
279
+ * served from the SWIRL index; results from connected sources arrive in the
280
+ * same response under the `swirl-federated` type.
281
+ *
282
+ * @public
283
+ */
284
+ declare class SwirlSearchEngine implements SearchEngine {
285
+ private readonly options;
286
+ private readonly logger;
287
+ private readonly client;
288
+ private readonly preTag;
289
+ private readonly postTag;
290
+ private constructor();
291
+ static fromConfig(config: Config, deps: SwirlSearchEngineOptions): Promise<SwirlSearchEngine>;
292
+ /**
293
+ * Mirrors the app-config tuning block to SWIRL so that relevance is
294
+ * configured in one place. A SWIRL that is not up yet, or an older SWIRL
295
+ * that does not know the endpoint, must not stop the backend from booting.
296
+ *
297
+ * SWIRL answers with the effective tuning in its own flat form plus
298
+ * `accepted_keys`, naming every key it took in the shape it was sent, and a
299
+ * `bm25` notice when it stored BM25 parameters it cannot apply. Both are
300
+ * logged, because a tuning block that is accepted by Backstage and then
301
+ * quietly dropped by SWIRL is exactly the failure this call exists to make
302
+ * visible. A 400 names the keys SWIRL did not recognise; that is a warning,
303
+ * not a boot failure.
304
+ */
305
+ private pushTuning;
306
+ translator(query: SearchQuery, options: SwirlQueryTranslatorOptions): ConcreteSwirlQuery;
307
+ setTranslator(translator: SwirlQueryTranslator): void;
308
+ getIndexer(type: string): Promise<Writable>;
309
+ query(query: SearchQuery, options?: QueryRequestOptions): Promise<IndexableResultSet>;
310
+ /**
311
+ * Page 0 federates: SWIRL runs the query across the Backstage index and,
312
+ * when the federated lane is active, the connected providers too.
313
+ */
314
+ private fetchFirstPage;
315
+ /**
316
+ * Page N is a database read in SWIRL, not a second federation. That keeps
317
+ * the paging loop in Backstage's AuthorizedSearchEngine cheap.
318
+ */
319
+ private fetchResultPage;
320
+ /**
321
+ * The search router hands the engine a plugin token minted per request,
322
+ * carrying the caller's identity in its `obo` claim; that token is what
323
+ * SWIRL verifies. Programmatic callers that reach the engine directly get
324
+ * a freshly minted one instead.
325
+ */
326
+ private resolveToken;
327
+ /**
328
+ * SWIRL reports a type with no live index either as a 404 with an
329
+ * `missing_index` error body, or as a structured `__MISSING_INDEX__` entry
330
+ * in the response messages. Either way the caller asked for something that
331
+ * has never been indexed, which is worth saying out loud rather than
332
+ * returning an empty result set or a bare 500.
333
+ */
334
+ private assertIndexPresent;
335
+ private toIndexableResult;
336
+ private toHighlight;
337
+ /** Removes the configured marker pair, leaving the text it wrapped. */
338
+ private stripMarkers;
339
+ /**
340
+ * SWIRL wraps hits in a configurable marker pair, `<em>` and `</em>` out of
341
+ * the box. Backstage expects the engine's own per-instance tags instead, so
342
+ * that a document body containing the marker cannot forge a highlight.
343
+ *
344
+ * The `maxChars` budget counts visible characters, not tags, and the walk
345
+ * never emits an unbalanced tag: a snippet cut short inside a hit closes it.
346
+ */
347
+ private rewriteHighlight;
348
+ }
349
+ /** @public */
350
+ declare function decodePageCursor(pageCursor?: string): SwirlPageCursor | undefined;
351
+ /** @public */
352
+ declare function encodePageCursor(cursor: SwirlPageCursor): string;
353
+ /** @public */
354
+ declare function readSwirlConfig(config: Config): SwirlEngineConfig;
355
+
356
+ /**
357
+ * Options for {@link SwirlIndexer}.
358
+ *
359
+ * @public
360
+ */
361
+ type SwirlIndexerOptions = {
362
+ type: string;
363
+ batchSize: number;
364
+ client: SwirlClient;
365
+ logger: LoggerService;
366
+ /** Retries after the first attempt, on 5xx and transport errors. Default 3. */
367
+ maxRetries?: number;
368
+ /** First backoff step in ms; doubles per retry. Default 250. */
369
+ retryBaseDelayMs?: number;
370
+ };
371
+ /**
372
+ * Writes one generation of documents of a single type into SWIRL, using the
373
+ * generation lifecycle of the SWIRL ingest API: begin, docs, then finalize or
374
+ * abort. The live generation is only replaced by a successful finalize, so a
375
+ * stream that dies half way through leaves the served index untouched.
376
+ *
377
+ * @public
378
+ */
379
+ declare class SwirlIndexer extends BatchSearchEngineIndexer {
380
+ private readonly type;
381
+ private readonly client;
382
+ private readonly logger;
383
+ private readonly maxRetries;
384
+ private readonly retryBaseDelayMs;
385
+ private generation?;
386
+ private numRecords;
387
+ private settled;
388
+ constructor(options: SwirlIndexerOptions);
389
+ initialize(): Promise<void>;
390
+ index(documents: IndexableDocument[]): Promise<void>;
391
+ finalize(): Promise<void>;
392
+ /** Best effort: an abort that fails is logged, never rethrown. */
393
+ private abort;
394
+ private generationUrl;
395
+ private postWithRetry;
396
+ }
397
+
398
+ /**
399
+ * Options for {@link SwirlNoopIndexer}.
400
+ *
401
+ * @public
402
+ */
403
+ type SwirlNoopIndexerOptions = {
404
+ type: string;
405
+ logger: LoggerService;
406
+ };
407
+ /**
408
+ * The indexer handed back for the federated document type. The federated lane
409
+ * has nothing to index: its collator yields zero documents and exists only so
410
+ * the type is registered. Anything written here is dropped, so a stray
411
+ * document can never reach the SWIRL ingest API under this type.
412
+ *
413
+ * @public
414
+ */
415
+ declare class SwirlNoopIndexer extends BatchSearchEngineIndexer {
416
+ private readonly type;
417
+ private readonly logger;
418
+ private numRecords;
419
+ constructor(options: SwirlNoopIndexerOptions);
420
+ initialize(): Promise<void>;
421
+ index(documents: IndexableDocument[]): Promise<void>;
422
+ finalize(): Promise<void>;
423
+ }
424
+
425
+ /**
426
+ * Options for {@link SwirlFederatedCollatorFactory}.
427
+ *
428
+ * @public
429
+ */
430
+ type SwirlFederatedCollatorFactoryOptions = {
431
+ logger: LoggerService;
432
+ };
433
+ /**
434
+ * A collator that yields no documents.
435
+ *
436
+ * Federated results are produced by SWIRL at query time and are never indexed
437
+ * by Backstage, but they still need a registered document type: the type is
438
+ * what makes them appear in `getDocumentTypes()`, survive
439
+ * `AuthorizedSearchEngine` when permissions are on, and show up as a filter in
440
+ * the search UI. Registering an empty collator is the cheapest way to declare
441
+ * the type.
442
+ *
443
+ * @public
444
+ */
445
+ declare class SwirlFederatedCollatorFactory implements DocumentCollatorFactory {
446
+ readonly type = "swirl-federated";
447
+ private readonly logger;
448
+ private constructor();
449
+ static fromConfig(_config: Config, options: SwirlFederatedCollatorFactoryOptions): SwirlFederatedCollatorFactory;
450
+ getCollator(): Promise<Readable>;
451
+ private execute;
452
+ }
453
+
454
+ export { MISSING_INDEX_ERROR_NAME, SWIRL_FEDERATED_TYPE, SWIRL_HIGHLIGHT_END_MARKER, SWIRL_HIGHLIGHT_START_MARKER, SWIRL_INDEX_PROVIDER_TAG, SwirlClient, SwirlFederatedCollatorFactory, SwirlIndexer, SwirlNoopIndexer, SwirlSearchEngine, decodePageCursor, searchModuleSwirlEngine as default, encodePageCursor, readSwirlConfig, swirlResultScore };
455
+ export type { ConcreteSwirlQuery, SwirlBackstagePayload, SwirlClientOptions, SwirlEngineConfig, SwirlFederatedCollatorFactoryOptions, SwirlIndexerOptions, SwirlNoopIndexerOptions, SwirlPageCursor, SwirlQueryTranslator, SwirlQueryTranslatorOptions, SwirlRequestResult, SwirlResponse, SwirlResult, SwirlSearchEngineOptions, SwirlTuning };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var alpha = require('@backstage/plugin-search-backend-node/alpha');
5
+ var SwirlFederatedCollatorFactory = require('./collators/SwirlFederatedCollatorFactory.cjs.js');
6
+ var SwirlSearchEngine = require('./engines/SwirlSearchEngine.cjs.js');
7
+
8
+ const FEDERATED_SCHEDULE = {
9
+ frequency: { hours: 24 },
10
+ timeout: { minutes: 1 },
11
+ initialDelay: { seconds: 3 }
12
+ };
13
+ const searchModuleSwirlEngine = backendPluginApi.createBackendModule({
14
+ pluginId: "search",
15
+ moduleId: "swirl-engine",
16
+ register(env) {
17
+ env.registerInit({
18
+ deps: {
19
+ searchEngineRegistry: alpha.searchEngineRegistryExtensionPoint,
20
+ indexRegistry: alpha.searchIndexRegistryExtensionPoint,
21
+ config: backendPluginApi.coreServices.rootConfig,
22
+ logger: backendPluginApi.coreServices.logger,
23
+ auth: backendPluginApi.coreServices.auth,
24
+ scheduler: backendPluginApi.coreServices.scheduler
25
+ },
26
+ async init({
27
+ searchEngineRegistry,
28
+ indexRegistry,
29
+ config,
30
+ logger,
31
+ auth,
32
+ scheduler
33
+ }) {
34
+ if (!config.getOptionalConfig("search.swirl")) {
35
+ logger.warn(
36
+ 'No configuration found under "search.swirl". Skipping registration of the SWIRL search engine.'
37
+ );
38
+ return;
39
+ }
40
+ searchEngineRegistry.setSearchEngine(
41
+ await SwirlSearchEngine.SwirlSearchEngine.fromConfig(config, { logger, auth })
42
+ );
43
+ const federatedEnabled = config.getOptionalBoolean("search.swirl.federated.enabled") ?? true;
44
+ if (federatedEnabled) {
45
+ indexRegistry.addCollator({
46
+ schedule: scheduler.createScheduledTaskRunner(FEDERATED_SCHEDULE),
47
+ factory: SwirlFederatedCollatorFactory.SwirlFederatedCollatorFactory.fromConfig(config, {
48
+ logger
49
+ })
50
+ });
51
+ } else {
52
+ logger.info(
53
+ 'The SWIRL federated document type is disabled by "search.swirl.federated.enabled"; federated results will not be returned.'
54
+ );
55
+ }
56
+ }
57
+ });
58
+ }
59
+ });
60
+
61
+ exports.searchModuleSwirlEngine = searchModuleSwirlEngine;
62
+ //# sourceMappingURL=module.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright 2026 SWIRL AI Connect\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport {\n searchEngineRegistryExtensionPoint,\n searchIndexRegistryExtensionPoint,\n} from '@backstage/plugin-search-backend-node/alpha';\nimport { SwirlFederatedCollatorFactory } from './collators/SwirlFederatedCollatorFactory';\nimport { SwirlSearchEngine } from './engines/SwirlSearchEngine';\n\n/**\n * The federated collator produces nothing, so it only needs to run often\n * enough to keep the document type registered across restarts.\n */\nconst FEDERATED_SCHEDULE = {\n frequency: { hours: 24 },\n timeout: { minutes: 1 },\n initialDelay: { seconds: 3 },\n};\n\n/**\n * Search backend module for the SWIRL engine.\n *\n * @public\n */\nexport const searchModuleSwirlEngine = createBackendModule({\n pluginId: 'search',\n moduleId: 'swirl-engine',\n register(env) {\n env.registerInit({\n deps: {\n searchEngineRegistry: searchEngineRegistryExtensionPoint,\n indexRegistry: searchIndexRegistryExtensionPoint,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n scheduler: coreServices.scheduler,\n },\n async init({\n searchEngineRegistry,\n indexRegistry,\n config,\n logger,\n auth,\n scheduler,\n }) {\n // Without config there is nothing to point at. Warn and step aside so\n // the search plugin keeps whatever engine it already has, rather than\n // throwing on a second engine registration.\n if (!config.getOptionalConfig('search.swirl')) {\n logger.warn(\n 'No configuration found under \"search.swirl\". Skipping registration of the SWIRL search engine.',\n );\n return;\n }\n\n searchEngineRegistry.setSearchEngine(\n await SwirlSearchEngine.fromConfig(config, { logger, auth }),\n );\n\n const federatedEnabled =\n config.getOptionalBoolean('search.swirl.federated.enabled') ?? true;\n\n if (federatedEnabled) {\n indexRegistry.addCollator({\n schedule: scheduler.createScheduledTaskRunner(FEDERATED_SCHEDULE),\n factory: SwirlFederatedCollatorFactory.fromConfig(config, {\n logger,\n }),\n });\n } else {\n logger.info(\n 'The SWIRL federated document type is disabled by \"search.swirl.federated.enabled\"; federated results will not be returned.',\n );\n }\n },\n });\n },\n});\n\nexport default searchModuleSwirlEngine;\n"],"names":["createBackendModule","searchEngineRegistryExtensionPoint","searchIndexRegistryExtensionPoint","coreServices","SwirlSearchEngine","SwirlFederatedCollatorFactory"],"mappings":";;;;;;;AA+BA,MAAM,kBAAA,GAAqB;AAAA,EACzB,SAAA,EAAW,EAAE,KAAA,EAAO,EAAA,EAAG;AAAA,EACvB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,EACtB,YAAA,EAAc,EAAE,OAAA,EAAS,CAAA;AAC3B,CAAA;AAOO,MAAM,0BAA0BA,oCAAA,CAAoB;AAAA,EACzD,QAAA,EAAU,QAAA;AAAA,EACV,QAAA,EAAU,cAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,oBAAA,EAAsBC,wCAAA;AAAA,QACtB,aAAA,EAAeC,uCAAA;AAAA,QACf,QAAQC,6BAAA,CAAa,UAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,WAAWA,6BAAA,CAAa;AAAA,OAC1B;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,oBAAA;AAAA,QACA,aAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA;AAAA,OACF,EAAG;AAID,QAAA,IAAI,CAAC,MAAA,CAAO,iBAAA,CAAkB,cAAc,CAAA,EAAG;AAC7C,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WACF;AACA,UAAA;AAAA,QACF;AAEA,QAAA,oBAAA,CAAqB,eAAA;AAAA,UACnB,MAAMC,mCAAA,CAAkB,UAAA,CAAW,QAAQ,EAAE,MAAA,EAAQ,MAAM;AAAA,SAC7D;AAEA,QAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,kBAAA,CAAmB,gCAAgC,CAAA,IAAK,IAAA;AAEjE,QAAA,IAAI,gBAAA,EAAkB;AACpB,UAAA,aAAA,CAAc,WAAA,CAAY;AAAA,YACxB,QAAA,EAAU,SAAA,CAAU,yBAAA,CAA0B,kBAAkB,CAAA;AAAA,YAChE,OAAA,EAASC,2DAAA,CAA8B,UAAA,CAAW,MAAA,EAAQ;AAAA,cACxD;AAAA,aACD;AAAA,WACF,CAAA;AAAA,QACH,CAAA,MAAO;AACL,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WACF;AAAA,QACF;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}