// The search: one collection, one query, cited answers. The query leaves the process (the procedure // is an effect), the ranking is the platform's service, and every answer carries where it came from: // the file, the chunk, and the snippet a person reads. import { AppError, forbidden, internal } from "@conusai-kit/core" import { getFile } from "@conusai-kit/files" import { documents, MAX_SEARCH_RESULTS, type SearchResult } from "../contract/index.js" import { requireCollection } from "./collections.js" import { type KnowledgeDeps, type KnowledgeScope, knowledgeEnabled } from "./ports.js" export async function searchKnowledge( deps: KnowledgeDeps, scope: KnowledgeScope, input: { collectionName: string; query: string; limit?: number | undefined }, ): Promise<{ results: SearchResult[] }> { if (!(await knowledgeEnabled(deps, scope))) throw forbidden("knowledge is switched off for this project") await requireCollection(deps, scope, input.collectionName) const service = deps.search if (!service) throw internal("the knowledge search service is not bound in this process") const limit = Math.min(input.limit ?? 10, MAX_SEARCH_RESULTS) const hits = await service.search(scope, { collectionName: input.collectionName, query: input.query, limit, actorId: scope.actor.userId, }) const results: SearchResult[] = [] for (const hit of hits) { // The current scoped source of truth revokes a citation before the remote deletion job finishes. const current = ( await deps.data .forScope(scope) .of(documents) .list({ where: [ { field: "collectionName", op: "eq", value: input.collectionName }, { field: "fileId", op: "eq", value: hit.fileId }, { field: "status", op: "eq", value: "indexed" }, ], limit: 1, }) ).items[0] if (!current || hit.collectionName !== input.collectionName) continue // The original file can be deleted before its outbox cleanup is delivered. // Current scoped file visibility is mandatory, independent of stale index rows. try { await getFile(deps.files, scope, hit.fileId) } catch (error) { if (error instanceof AppError && error.status === 404) continue throw error } results.push({ fileId: hit.fileId, filename: hit.filename, collectionName: hit.collectionName, snippet: hit.snippet, score: hit.score, chunk: hit.chunk, ...(hit.page === undefined ? {} : { page: hit.page }), ...(hit.blockId === undefined ? {} : { blockId: hit.blockId }), ...(hit.chunkId === undefined ? {} : { chunkId: hit.chunkId }), }) } return { results } }