{"version":3,"file":"search.mjs","names":[],"sources":["../src/search.ts"],"sourcesContent":["import { Effect, Match } from 'effect'\nimport type { IndexedKnowledgeDocument, KnowledgeChunk, KnowledgeSearchScope } from './documents.ts'\nimport { KnowledgeEmbedder } from './embeddings.ts'\nimport { KnowledgeSearchError } from './errors.ts'\nimport { SearchIndexStore } from './store.ts'\n\nexport type KnowledgeSearchInput = {\n  readonly scope: KnowledgeSearchScope\n  readonly query: string\n  readonly limit?: number\n  readonly minScore?: number\n  readonly contextChunks?: number\n  readonly mode?: KnowledgeSearchMode\n  readonly vectorLimit?: number\n  readonly textLimit?: number\n}\n\nexport type KnowledgeSearchMode = 'vector' | 'hybrid'\n\nexport type KnowledgeSearchScores = {\n  readonly vector?: number\n  readonly text?: number\n  readonly fused?: number\n}\n\nexport type KnowledgeSearchResult = {\n  readonly chunk: KnowledgeChunk\n  readonly score: number\n  readonly document: IndexedKnowledgeDocument\n  readonly context?: ReadonlyArray<KnowledgeChunk>\n  readonly scores?: KnowledgeSearchScores\n}\n\nexport type KnowledgeSearchContext = {\n  readonly query: string\n  readonly results: ReadonlyArray<KnowledgeSearchResult>\n  readonly text: string\n}\n\nexport type KnowledgeSearcher = {\n  readonly search: (\n    input: KnowledgeSearchInput\n  ) => Effect.Effect<ReadonlyArray<KnowledgeSearchResult>, KnowledgeSearchError>\n}\n\nconst packedResultText = (result: KnowledgeSearchResult) =>\n  result.context?.map(chunk => chunk.content).join('\\n\\n') ?? result.chunk.content\n\nexport const packKnowledgeSearchContext = (\n  query: string,\n  results: ReadonlyArray<KnowledgeSearchResult>\n): KnowledgeSearchContext => ({\n  query,\n  results,\n  text: results.map(packedResultText).join('\\n\\n')\n})\n\nconst scopeIds = (scope: KnowledgeSearchScope): ReadonlyArray<string> =>\n  Match.value(scope).pipe(\n    Match.tagsExhaustive({\n      KnowledgeScope: ({ id }) => [id],\n      KnowledgeScopes: ({ ids }) => ids\n    })\n  )\n\nconst defaultHybridCandidateLimit = (limit: number) => Math.max(limit * 5, 40)\n\nconst reciprocalRankFusionScore = (rank: number, rankConstant: number) => 1 / (rankConstant + rank)\n\nexport const fuseKnowledgeSearchResults = (input: {\n  readonly vectorResults: ReadonlyArray<KnowledgeSearchResult>\n  readonly textResults: ReadonlyArray<KnowledgeSearchResult>\n  readonly limit: number\n  readonly rankConstant?: number\n}): ReadonlyArray<KnowledgeSearchResult> => {\n  const rankConstant = input.rankConstant ?? 60\n\n  const fused = new Map<\n    string,\n    {\n      readonly result: KnowledgeSearchResult\n      readonly vector?: number\n      readonly text?: number\n      readonly fused: number\n    }\n  >()\n\n  const addResults = (results: ReadonlyArray<KnowledgeSearchResult>, kind: 'vector' | 'text') => {\n    results.forEach((result, index) => {\n      const rank = index + 1\n      const previous = fused.get(result.chunk.id)\n      const nextFused = (previous?.fused ?? 0) + reciprocalRankFusionScore(rank, rankConstant)\n      fused.set(result.chunk.id, {\n        result: previous?.result ?? result,\n        vector: kind === 'vector' ? result.score : previous?.vector,\n        text: kind === 'text' ? result.score : previous?.text,\n        fused: nextFused\n      })\n    })\n  }\n\n  addResults(input.vectorResults, 'vector')\n  addResults(input.textResults, 'text')\n\n  return Array.from(fused.values())\n    .sort((left, right) => right.fused - left.fused)\n    .slice(0, input.limit)\n    .map(item => ({\n      ...item.result,\n      score: item.fused,\n      scores: { vector: item.vector, text: item.text, fused: item.fused }\n    }))\n}\n\nconst validateSearchInput = (input: KnowledgeSearchInput) => {\n  const query = input.query.trim()\n  const ids = scopeIds(input.scope)\n  const limit = input.limit ?? 10\n  const contextChunks = input.contextChunks ?? 0\n  const mode = input.mode ?? 'hybrid'\n\n  const vectorLimit =\n    input.vectorLimit ?? (mode === 'hybrid' ? defaultHybridCandidateLimit(limit) : limit)\n\n  const textLimit = input.textLimit ?? defaultHybridCandidateLimit(limit)\n\n  if (query.length === 0) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Search query is empty', stage: 'embed' })\n    )\n  }\n\n  if (ids.length === 0) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Search scope is empty', stage: 'store' })\n    )\n  }\n\n  if (!Number.isInteger(limit) || limit < 1) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Search limit must be positive', stage: 'store' })\n    )\n  }\n\n  if (!Number.isInteger(vectorLimit) || vectorLimit < 1) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Vector search limit must be positive', stage: 'store' })\n    )\n  }\n\n  if (!Number.isInteger(textLimit) || textLimit < 1) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Text search limit must be positive', stage: 'store' })\n    )\n  }\n\n  if (mode !== 'vector' && mode !== 'hybrid') {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Search mode is invalid', stage: 'store' })\n    )\n  }\n\n  if (!Number.isInteger(contextChunks) || contextChunks < 0) {\n    return Effect.fail(\n      new KnowledgeSearchError({\n        message: 'Context chunks must be zero or positive',\n        stage: 'store'\n      })\n    )\n  }\n\n  if (input.minScore !== undefined && !Number.isFinite(input.minScore)) {\n    return Effect.fail(\n      new KnowledgeSearchError({ message: 'Search minScore must be finite', stage: 'store' })\n    )\n  }\n\n  return Effect.succeed({ query, limit, contextChunks, mode, vectorLimit, textLimit })\n}\n\nconst searchVectorChunks = (input: {\n  readonly scope: KnowledgeSearchScope\n  readonly query: string\n  readonly limit: number\n  readonly minScore?: number\n}) =>\n  Effect.gen(function* () {\n    const store = yield* SearchIndexStore\n    const embedder = yield* KnowledgeEmbedder\n\n    const embedding = yield* embedder\n      .embedQuery(input.query)\n      .pipe(\n        Effect.mapError(\n          error =>\n            new KnowledgeSearchError({ message: error.message, stage: 'embed', cause: error })\n        )\n      )\n\n    return yield* store\n      .searchChunks({\n        scope: input.scope,\n        embedding,\n        limit: input.limit,\n        minScore: input.minScore\n      })\n      .pipe(\n        Effect.mapError(\n          error =>\n            new KnowledgeSearchError({ message: error.message, stage: 'store', cause: error })\n        )\n      )\n  })\n\nconst searchTextChunks = (input: {\n  readonly scope: KnowledgeSearchScope\n  readonly query: string\n  readonly limit: number\n}) =>\n  Effect.gen(function* () {\n    const store = yield* SearchIndexStore\n\n    return yield* store\n      .searchChunksByText({ scope: input.scope, query: input.query, limit: input.limit })\n      .pipe(\n        Effect.mapError(\n          error =>\n            new KnowledgeSearchError({ message: error.message, stage: 'store', cause: error })\n        )\n      )\n  })\n\nexport const searchKnowledge = (input: KnowledgeSearchInput) =>\n  Effect.gen(function* () {\n    const valid = yield* validateSearchInput(input)\n    yield* Effect.annotateCurrentSpan({\n      'knowledge_search.query_length': valid.query.length,\n      'knowledge_search.limit': valid.limit,\n      'knowledge_search.mode': valid.mode,\n      'knowledge_search.context_chunks': valid.contextChunks,\n      'knowledge_search.scope_count': scopeIds(input.scope).length\n    })\n    const store = yield* SearchIndexStore\n\n    const results: ReadonlyArray<KnowledgeSearchResult> =\n      valid.mode === 'vector'\n        ? yield* searchVectorChunks({\n            scope: input.scope,\n            query: valid.query,\n            limit: valid.vectorLimit,\n            minScore: input.minScore\n          })\n        : yield* Effect.gen(function* () {\n            const searches = yield* Effect.all(\n              {\n                vectorResults: searchVectorChunks({\n                  scope: input.scope,\n                  query: valid.query,\n                  limit: valid.vectorLimit,\n                  minScore: input.minScore\n                }),\n                textResults: searchTextChunks({\n                  scope: input.scope,\n                  query: valid.query,\n                  limit: valid.textLimit\n                })\n              },\n              { concurrency: 'unbounded' }\n            )\n\n            return fuseKnowledgeSearchResults({\n              vectorResults: searches.vectorResults,\n              textResults: searches.textResults,\n              limit: valid.limit\n            })\n          })\n\n    if (valid.contextChunks === 0) {\n      return results\n    }\n\n    return yield* Effect.forEach(results, result =>\n      store\n        .getContextChunks({\n          scopeId: result.chunk.scopeId,\n          documentId: result.chunk.documentId,\n          position: result.chunk.position,\n          contextChunks: valid.contextChunks\n        })\n        .pipe(\n          Effect.map(context => ({ ...result, context })),\n          Effect.mapError(\n            error =>\n              new KnowledgeSearchError({ message: error.message, stage: 'store', cause: error })\n          )\n        )\n    )\n  }).pipe(Effect.withSpan('knowledge_search.search'))\n"],"mappings":"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"}