/**
* Search utilities for Gemini API documentation.
*/
import { DOC_INDEX, DOCS_BASE_URL } from "../constants.js";
export interface SearchResult {
path: string;
url: string;
title: string;
category: string;
score: number;
matchedKeywords: string[];
}
/**
* Searches the documentation index for matching pages.
*/
export function searchDocs(query: string, maxResults: number = 10): SearchResult[] {
const queryLower = query.toLowerCase();
const queryTerms = queryLower.split(/\s+/).filter(t => t.length > 1);
const results: SearchResult[] = [];
for (const doc of DOC_INDEX) {
let score = 0;
const matchedKeywords: string[] = [];
// Check title match (highest weight)
const titleLower = doc.title.toLowerCase();
if (titleLower === queryLower) {
score += 100; // Exact title match
} else if (titleLower.includes(queryLower)) {
score += 50; // Partial title match
}
// Check each query term
for (const term of queryTerms) {
if (titleLower.includes(term)) {
score += 20;
}
if (doc.category.toLowerCase().includes(term)) {
score += 5;
}
if (doc.path.toLowerCase().includes(term)) {
score += 10;
}
}
// Check keyword matches
for (const keyword of doc.keywords) {
const keywordLower = keyword.toLowerCase();
if (keywordLower === queryLower) {
score += 80; // Exact keyword match
matchedKeywords.push(keyword);
} else if (keywordLower.includes(queryLower) || queryLower.includes(keywordLower)) {
score += 30; // Partial keyword match
matchedKeywords.push(keyword);
} else {
// Check individual terms
for (const term of queryTerms) {
if (keywordLower.includes(term)) {
score += 15;
if (!matchedKeywords.includes(keyword)) {
matchedKeywords.push(keyword);
}
}
}
}
}
if (score > 0) {
results.push({
path: doc.path,
url: doc.path ? `${DOCS_BASE_URL}/${doc.path}` : DOCS_BASE_URL,
title: doc.title,
category: doc.category,
score,
matchedKeywords: matchedKeywords.slice(0, 5),
});
}
}
// Sort by score descending
results.sort((a, b) => b.score - a.score);
return results.slice(0, maxResults);
}
/**
* Formats search results as Markdown.
*/
export function formatSearchResultsAsMarkdown(results: SearchResult[], query: string): string {
if (results.length === 0) {
return `# Search Results for "${query}"\n\nNo results found. Try different keywords or browse the documentation at ${DOCS_BASE_URL}`;
}
const lines: string[] = [];
lines.push(`# Search Results for "${query}"`);
lines.push("");
lines.push(`Found ${results.length} result(s)`);
lines.push("");
let currentCategory = "";
for (const result of results) {
if (result.category !== currentCategory) {
currentCategory = result.category;
lines.push(`## ${currentCategory}`);
lines.push("");
}
lines.push(`### [${result.title}](${result.url})`);
lines.push(`- **Path**: \`${result.path || "/"}\``);
if (result.matchedKeywords.length > 0) {
lines.push(`- **Matched**: ${result.matchedKeywords.join(", ")}`);
}
lines.push("");
}
return lines.join("\n");
}
/**
* Formats search results as JSON.
*/
export function formatSearchResultsAsJson(results: SearchResult[], query: string): string {
return JSON.stringify({
query,
total: results.length,
results: results.map(r => ({
title: r.title,
path: r.path,
url: r.url,
category: r.category,
matchedKeywords: r.matchedKeywords,
})),
}, null, 2);
}