import type { Edition, KnowledgeFile, KnowledgeScope, KnowledgeSection, SearchOptions, SearchResult, TableSchema } from "./types.ts"; import { loadKnowledge } from "./loader.ts"; export function searchKnowledge(query: string, options: SearchOptions = {}, basePath: string): SearchResult[] { if (!query.trim()) return []; const { tags, edition, scope, scopes, topK = 10, minScore = 0 } = options; const targetScopes = normalizeScopes(scopes ?? [scope ?? edition ?? "flagship"]); const results: SearchResult[] = []; const seenFiles = new Set(); for (const targetScope of targetScopes) { const knowledge = loadKnowledge(targetScope, basePath); for (const file of [...knowledge.common, ...knowledge.edition]) { if (seenFiles.has(file.path)) continue; seenFiles.add(file.path); results.push(...searchFile(file, query, tags)); } } return results .filter((result) => result.score >= minScore) .sort((a, b) => b.score - a.score) .slice(0, topK); } export function findTableSchema(tableName: string, edition: Edition, basePath: string): TableSchema | undefined { const knowledge = loadKnowledge(edition, basePath); return knowledge.tables.get(tableName.toUpperCase()); } function normalizeScopes(scopes: KnowledgeScope[]): KnowledgeScope[] { return [...new Set(scopes)]; } function searchFile(file: KnowledgeFile, query: string, tags?: string[]): SearchResult[] { if (tags?.length) { const hasTag = tags.some((tag) => file.tags.some((fileTag) => fileTag.toLowerCase().includes(tag.toLowerCase()))); if (!hasTag) return []; } const queryTokens = tokenize(query); const results: SearchResult[] = []; for (const section of file.sections) { const score = calculateScore(queryTokens, section, file); if (score <= 0) continue; results.push({ file, section, score, highlights: highlightMatches(section.content, query), }); } return results; } export function tokenize(text: string): string[] { return text .toLowerCase() .replace(/[^\w\u4e00-\u9fff]/g, " ") .split(/\s+/) .filter(Boolean); } function calculateScore(queryTokens: string[], section: KnowledgeSection, file: KnowledgeFile): number { let score = 0; const headingLower = section.heading.toLowerCase(); const contentLower = section.content.toLowerCase(); const titleLower = file.title.toLowerCase(); const fullQuery = queryTokens.join(" "); if (fullQuery && titleLower.includes(fullQuery)) score += 5; if (fullQuery && headingLower.includes(fullQuery)) score += 5; if (fullQuery && contentLower.includes(fullQuery)) score += 5; for (let i = 0; i < queryTokens.length; i++) { const token = queryTokens[i]; const positionWeight = 1 + 1 / (i + 1); if (titleLower.includes(token)) score += 3 * positionWeight; if (headingLower.includes(token)) score += 2 * positionWeight; if (contentLower.includes(token)) { const matchCount = contentLower.split(token).length - 1; score += (1 + Math.min(matchCount * 0.2, 2)) * positionWeight; } } return score; } function highlightMatches(content: string, query: string): string[] { const highlights: string[] = []; const queryTokens = tokenize(query); const lines = content.split("\n"); const matchedLines: number[] = []; for (let i = 0; i < lines.length; i++) { const lineLower = lines[i].toLowerCase(); if (queryTokens.some((token) => lineLower.includes(token))) { matchedLines.push(i); } } const visited = new Set(); for (const lineIdx of matchedLines) { if (visited.has(lineIdx)) continue; const start = Math.max(0, lineIdx - 2); const end = Math.min(lines.length - 1, lineIdx + 2); for (let i = start; i <= end; i++) visited.add(i); const context = lines.slice(start, end + 1).join("\n").trim(); if (context) highlights.push(context); } return highlights.slice(0, 3); }