180 lines
6.1 KiB
TypeScript
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;
|
|
}
|