Files
ra-h-os/app/api/health/vectors/route.ts
T

180 lines
6.1 KiB
TypeScript

import { NextResponse } from 'next/server';
import { getSQLiteClient } from '@/services/database/sqlite-client';
import { chunkService } from '@/services/database/chunks';
import { createEmbeddingProvider } from '@/services/embedding/provider';
import { createUtilityLlmProvider } from '@/services/llm/provider';
import { getVectorBackend } from '@/services/vectorBackend/factory';
import { getVectorBackendType } from '@/services/vectorBackend';
interface ChunkStats {
total_chunks: number;
vectorized_chunks: number | null;
missing_embeddings: number | null;
coverage_percentage: number | null;
}
interface VectorStats {
vec_chunks_count?: number;
matches_chunk_embeddings?: boolean;
extension_loaded?: boolean;
reason?: string;
error?: string;
suggestion?: string;
backend?: string;
status?: unknown;
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export async function GET() {
try {
const sqlite = getSQLiteClient();
// Test basic database connection
const connectionTest = await sqlite.testConnection();
if (!connectionTest) {
return NextResponse.json({
status: 'error',
message: 'Database connection failed',
details: null
});
}
const vectorBackend = await getVectorBackend();
const vectorBackendHealth = await vectorBackend.healthCheck();
const embeddingInfo = createEmbeddingProvider().info();
const utilityInfo = createUtilityLlmProvider().info();
const profileStatus = sqlite.getEmbeddingProfileStatus();
// Check if vector extension is loaded
const vectorExtensionTest = await sqlite.checkVectorExtension();
let vectorStats: VectorStats | null = null;
let chunkStats: ChunkStats | null = null;
let vectorHealth = vectorExtensionTest ? 'healthy' : 'unavailable';
try {
const totalChunks = await chunkService.getChunkCount();
chunkStats = {
total_chunks: totalChunks,
vectorized_chunks: null,
missing_embeddings: null,
coverage_percentage: null,
};
if (getVectorBackendType() === 'qdrant') {
vectorHealth = vectorBackendHealth.ok ? 'healthy' : 'unavailable';
vectorStats = {
backend: 'qdrant',
status: vectorBackendHealth,
matches_chunk_embeddings: !profileStatus.rebuild_required,
};
} else if (vectorExtensionTest) {
try {
const chunksWithoutEmbeddings = await chunkService.getChunksWithoutEmbeddings();
const vectorizedCount = totalChunks - chunksWithoutEmbeddings.length;
const result = sqlite.query('SELECT COUNT(*) as count FROM vec_chunks');
const vecCount = Number(result.rows[0].count);
chunkStats = {
total_chunks: totalChunks,
vectorized_chunks: vectorizedCount,
missing_embeddings: chunksWithoutEmbeddings.length,
coverage_percentage: totalChunks > 0 ? Math.round((vectorizedCount / totalChunks) * 100) : 0
};
vectorStats = {
vec_chunks_count: vecCount,
matches_chunk_embeddings: vecCount === vectorizedCount
};
vectorHealth = vecCount === vectorizedCount ? 'healthy' : 'inconsistent';
} catch (vecError: unknown) {
vectorHealth = 'corrupted';
vectorStats = {
error: errorMessage(vecError),
suggestion: 'Vector table may be corrupted and need recreation'
};
}
} else {
vectorHealth = 'unavailable';
vectorStats = {
extension_loaded: false,
reason: 'Vector extension unavailable in this environment.',
};
}
} catch (error: unknown) {
return NextResponse.json({
status: 'error',
message: 'Failed to collect vector statistics',
details: errorMessage(error)
});
}
return NextResponse.json({
status: 'success',
data: {
database_connected: connectionTest,
vector_extension_loaded: vectorExtensionTest,
vector_capability: {
available: vectorBackendHealth.ok,
backend: getVectorBackendType(),
detail: vectorBackendHealth.detail,
dimensions: vectorBackendHealth.dimensions,
},
utility_llm: utilityInfo,
embedding_provider: embeddingInfo,
embedding_profile: profileStatus,
vector_health: vectorHealth,
chunk_stats: chunkStats,
vector_stats: vectorStats,
recommendations: generateRecommendations(vectorHealth, chunkStats, vectorStats, profileStatus)
}
});
} catch (error: unknown) {
console.error('Vector health check failed:', error);
return NextResponse.json({
status: 'error',
message: 'Health check failed',
details: errorMessage(error)
});
}
}
function generateRecommendations(
vectorHealth: string,
chunkStats: ChunkStats | null,
vectorStats: VectorStats | null,
profileStatus?: { rebuild_required?: boolean; reason?: string }
): string[] {
const recommendations: string[] = [];
if (vectorHealth === 'corrupted') {
recommendations.push('Vector tables are corrupted - restart the application to trigger automatic healing');
}
if (vectorHealth === 'unavailable') {
recommendations.push('Semantic/vector search is unavailable. Install sqlite-vec for your platform or switch to Qdrant.');
}
if (chunkStats && typeof chunkStats.coverage_percentage === 'number' && chunkStats.coverage_percentage < 95) {
recommendations.push(`${chunkStats.missing_embeddings} chunks missing embeddings - consider running embedding generation`);
}
if (vectorStats && !vectorStats.matches_chunk_embeddings) {
recommendations.push('Vector count does not match chunk embeddings - database inconsistency detected');
}
if (profileStatus?.rebuild_required) {
recommendations.push(profileStatus.reason || 'Embedding profile changed - rebuild embeddings before semantic search');
}
if (recommendations.length === 0) {
recommendations.push('Vector search system is healthy');
}
return recommendations;
}