lorehub
Version:
Capture and surface the collective wisdom of your codebase
165 lines • 8.11 kB
JavaScript
import React from 'react';
import { render } from 'ink';
import { Database } from '../../db/database.js';
import { FactsView } from '../components/FactsView.js';
import { getDbPath } from '../utils/db-config.js';
export async function renderSearch(options) {
const dbPath = getDbPath();
const db = new Database(dbPath);
// Check if we can use interactive mode
if (!process.stdin.isTTY) {
// Non-interactive fallback
try {
const project = db.findProjectByPath(process.cwd());
let results = [];
const projects = db.listProjects();
const currentProject = project;
// Filter projects based on options
let projectsToSearch = projects;
if (options.currentProjectOnly) {
if (!currentProject) {
console.log('No LoreHub project found in current directory.');
return;
}
projectsToSearch = [currentProject];
}
else if (options.projectPath) {
const specificProject = db.findProjectByPath(options.projectPath);
if (!specificProject) {
console.log(`No LoreHub project found at path: ${options.projectPath}`);
return;
}
projectsToSearch = [specificProject];
}
// Search selected projects
for (const proj of projectsToSearch) {
let projectFacts;
if (options.hybrid) {
// Use hybrid search - combine keyword and semantic results
const keywordResults = db.searchFacts(proj.id, options.query);
const semanticResults = await db.semanticSearchLores(options.query, {
realmId: proj.id,
includeScore: true
});
// Create a map to track unique facts and combine scores
const factMap = new Map();
// Add keyword results with base score
keywordResults.forEach((fact) => {
factMap.set(fact.id, {
...fact,
keywordScore: 1.0, // Binary: found or not
semanticScore: 0,
hybridScore: 0.3 // Initial score from keyword match
});
});
// Add or update with semantic scores
semanticResults.forEach((fact) => {
const existing = factMap.get(fact.id);
if (existing) {
// Fact found in both searches - combine scores
existing.semanticScore = fact.similarity || 0;
existing.hybridScore = 0.3 * existing.keywordScore + 0.7 * existing.semanticScore;
existing.similarity = existing.hybridScore; // For display
}
else {
// Only found in semantic search
factMap.set(fact.id, {
...fact,
keywordScore: 0,
semanticScore: fact.similarity || 0,
hybridScore: 0.7 * (fact.similarity || 0),
similarity: 0.7 * (fact.similarity || 0) // For display
});
}
});
// Convert map to array and sort by hybrid score
projectFacts = Array.from(factMap.values())
.sort((a, b) => (b.hybridScore || 0) - (a.hybridScore || 0));
}
else if (options.semantic) {
// Use semantic search
// Note: semanticSearchFacts expects distance threshold, not similarity
// Don't pass threshold to get all results, we'll filter by similarity later
projectFacts = await db.semanticSearchLores(options.query, {
realmId: proj.id,
includeScore: true
});
}
else {
// Use traditional keyword search
projectFacts = db.searchFacts(proj.id, options.query);
}
// Add project info to each fact for display
results.push(...projectFacts.map(f => ({
...f,
projectName: proj.name,
projectPath: proj.path,
isCurrentProject: currentProject?.id === proj.id
})));
}
// Apply filters
if (options.type) {
results = results.filter(f => f.type === options.type);
}
if (options.province) {
results = results.filter(f => f.provinces.includes(options.province));
}
// Apply similarity threshold filter for semantic or hybrid search
if ((options.semantic || options.hybrid) && options.threshold !== undefined) {
const threshold = options.threshold;
results = results.filter(f => f.similarity !== undefined && f.similarity >= threshold);
}
// Sort by current project first, then by creation date descending
results.sort((a, b) => {
// Prioritize current project
if (a.isCurrentProject && !b.isCurrentProject)
return -1;
if (!a.isCurrentProject && b.isCurrentProject)
return 1;
// Then by date
return b.createdAt.getTime() - a.createdAt.getTime();
});
if (results.length === 0) {
console.log(`No facts found matching "${options.query}"`);
if (options.type)
console.log(`Filter: type = ${options.type}`);
if (options.province)
console.log(`Filter: province = ${options.province}`);
}
else {
const searchMode = options.hybrid ? 'hybrid' : (options.semantic ? 'semantic' : 'keyword');
console.log(`\nFound ${results.length} fact${results.length === 1 ? '' : 's'} matching "${options.query}" (${searchMode} search):\n`);
results.forEach((fact, index) => {
const projectIndicator = fact.isCurrentProject ? ' ⭐' : '';
console.log(`${index + 1}. [${fact.type}] ${fact.content}`);
console.log(` Project: ${fact.projectName}${projectIndicator} (${fact.projectPath})`);
if (fact.why) {
console.log(` Why: ${fact.why}`);
}
if ((options.semantic || options.hybrid) && fact.similarity !== undefined) {
console.log(` Similarity: ${(fact.similarity * 100).toFixed(1)}%`);
}
console.log(` Confidence: ${fact.confidence}%`);
console.log(` Created: ${fact.createdAt.toLocaleString()}`);
if (fact.sigils.length > 0) {
console.log(` Sigils: ${fact.sigils.join(', ')}`);
}
console.log('');
});
}
}
finally {
db.close();
}
return;
}
// Interactive mode with Ink
const { waitUntilExit } = render(React.createElement(FactsView, { db: db, projectPath: process.cwd(), initialQuery: options.query, type: options.type, province: options.province, filterProjectPath: options.projectPath, currentProjectOnly: options.currentProjectOnly }));
try {
await waitUntilExit();
}
finally {
db.close();
}
}
//# sourceMappingURL=search.js.map