feat(leann-phase2): implement hybrid vector storage and graph-based search

- [x] Add AST-aware code chunking for Go, Python, and TypeScript using tree-sitter
- [x] Implement LEANN-inspired hybrid vector storage with hub detection and selective embedding storage (60-80% savings)
- [x] Add observation relationship graph with CSR format and edge detection (file overlap, semantic similarity, temporal, concept)
- [x] Implement graph-aware search with two-level traversal and relationship-based ranking
- [x] Add auto-tuning system for dynamic hub threshold adjustment based on query performance
- [x] Add comprehensive metrics tracking for vector storage, queries, latency, and graph traversals
- [x] Update configuration system with graph and hybrid storage settings
- [x] Add graph stats and vector metrics endpoints to worker service
- [x] Enhance UI sidebar with advanced metrics display and graph visualization
- [x] Optimize struct field alignment throughout codebase for memory efficiency
- [x] Update documentation with LEANN Phase 2 features and performance benefits
- [x] Add tree-sitter dependency for AST parsing
This commit is contained in:
2026-01-07 20:43:10 +00:00
parent 7ab4b07cf2
commit 74ae8ed4c1
83 changed files with 5190 additions and 603 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-mnemonic-dashboard",
"version": "8fe9ea5-dirty",
"version": "v0.10.5-1-g7ab4b07-dirty",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-mnemonic-dashboard",
"version": "8fe9ea5-dirty",
"version": "v0.10.5-1-g7ab4b07-dirty",
"dependencies": {
"vis-data": "^7.1.9",
"vis-network": "^9.1.9",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mnemonic-dashboard",
"version": "8fe9ea5-dirty",
"version": "v0.10.5-1-g7ab4b07-dirty",
"private": true,
"type": "module",
"scripts": {
+127
View File
@@ -2,6 +2,7 @@
import { ref, computed } from 'vue'
import type { Stats, SelfCheckResponse } from '@/types'
import ProjectFilter from './ProjectFilter.vue'
import { useGraphMetrics } from '@/composables'
const props = defineProps<{
stats: Stats | null
@@ -18,12 +19,21 @@ defineEmits<{
// Collapse state - persisted in localStorage
const isCollapsed = ref(localStorage.getItem('sidebar-collapsed') === 'true')
const metricsExpanded = ref(localStorage.getItem('metrics-expanded') === 'true')
// Graph metrics composable
const { graphStats, vectorMetrics, loading: metricsLoading, refresh: refreshMetrics } = useGraphMetrics()
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value
localStorage.setItem('sidebar-collapsed', String(isCollapsed.value))
}
function toggleMetrics() {
metricsExpanded.value = !metricsExpanded.value
localStorage.setItem('metrics-expanded', String(metricsExpanded.value))
}
function formatNumber(n: number): string {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
@@ -205,6 +215,99 @@ function getStatusColor(status: string): string {
</div>
</div>
<!-- Advanced Metrics -->
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50">
<button
@click="toggleMetrics"
class="w-full flex items-center justify-between p-4 hover:bg-slate-700/30 transition-colors rounded-lg"
>
<div class="flex items-center gap-2">
<i class="fas fa-chart-line text-violet-400" />
<h3 class="text-sm font-semibold text-white">Advanced Metrics</h3>
</div>
<i
:class="[
'fas text-slate-400 transition-transform duration-200',
metricsExpanded ? 'fa-chevron-up' : 'fa-chevron-down'
]"
/>
</button>
<Transition name="expand">
<div v-show="metricsExpanded" class="px-4 pb-4 space-y-4">
<!-- Loading State -->
<div v-if="metricsLoading" class="text-center py-4">
<i class="fas fa-spinner fa-spin text-slate-400" />
<p class="text-slate-500 text-sm mt-2">Loading metrics...</p>
</div>
<!-- Graph Stats -->
<div v-else-if="graphStats?.enabled">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-400 uppercase tracking-wide">Graph</span>
<button
@click="refreshMetrics"
class="text-xs text-violet-400 hover:text-violet-300 transition-colors"
title="Refresh metrics"
>
<i class="fas fa-sync-alt" />
</button>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Nodes</span>
<span class="text-white font-medium">{{ formatNumber(graphStats.nodeCount) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Edges</span>
<span class="text-white font-medium">{{ formatNumber(graphStats.edgeCount) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Avg Degree</span>
<span class="text-white font-medium">{{ graphStats.avgDegree.toFixed(1) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Max Degree</span>
<span class="text-white font-medium">{{ graphStats.maxDegree }}</span>
</div>
</div>
<!-- Vector Metrics -->
<div v-if="vectorMetrics?.enabled" class="mt-4 pt-4 border-t border-slate-700/50">
<div class="text-xs text-slate-400 uppercase tracking-wide mb-2">Vector Storage</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Savings</span>
<span class="text-green-400 font-medium">
{{ vectorMetrics.storage.savingsPercent.toFixed(1) }}%
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Queries</span>
<span class="text-white font-medium">{{ formatNumber(vectorMetrics.queries.total) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Cache Hit</span>
<span class="text-cyan-400 font-medium">
{{ (vectorMetrics.cache.hitRate * 100).toFixed(1) }}%
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Avg Latency</span>
<span class="text-white font-medium text-xs">{{ vectorMetrics.latency.avg }}</span>
</div>
</div>
</div>
</div>
<!-- Disabled State -->
<div v-else class="text-slate-500 text-sm py-2">
{{ graphStats?.message || 'Metrics not available' }}
</div>
</div>
</Transition>
</div>
<!-- Session Info -->
<div v-if="stats" class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
<div class="flex items-center gap-2 mb-3">
@@ -260,6 +363,30 @@ function getStatusColor(status: string): string {
>
<i class="fas fa-search text-cyan-400" />
</div>
<!-- Metrics indicator -->
<div
v-if="graphStats?.enabled"
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
:title="`${graphStats.nodeCount} nodes, ${graphStats.edgeCount} edges`"
>
<i class="fas fa-chart-line text-violet-400" />
</div>
</div>
</aside>
</template>
<style scoped>
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease;
overflow: hidden;
max-height: 500px;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
</style>
+1
View File
@@ -3,3 +3,4 @@ export { useStats } from './useStats'
export { useTimeline } from './useTimeline'
export { useUpdate } from './useUpdate'
export { useHealth } from './useHealth'
export { useGraphMetrics } from './useGraphMetrics'
+43
View File
@@ -0,0 +1,43 @@
import { ref, onMounted } from 'vue'
import type { GraphStats, VectorMetrics } from '@/types'
import { fetchGraphStats, fetchVectorMetrics } from '@/utils/api'
export function useGraphMetrics() {
const graphStats = ref<GraphStats | null>(null)
const vectorMetrics = ref<VectorMetrics | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
const refresh = async () => {
loading.value = true
error.value = null
try {
// Fetch both in parallel
const [graph, vector] = await Promise.all([
fetchGraphStats(),
fetchVectorMetrics()
])
graphStats.value = graph
vectorMetrics.value = vector
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch metrics'
console.error('[GraphMetrics] Error:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
refresh()
})
return {
graphStats,
vectorMetrics,
loading,
error,
refresh
}
}
+55
View File
@@ -63,3 +63,58 @@ export interface SelfCheckResponse {
uptime: string
components: ComponentHealth[]
}
export interface GraphStats {
enabled: boolean
nodeCount: number
edgeCount: number
avgDegree: number
maxDegree: number
minDegree: number
medianDegree: number
edgeTypes: Record<string, number>
config: {
maxHops: number
branchFactor: number
edgeWeight: number
rebuildIntervalMin: number
}
message?: string
}
export interface VectorMetrics {
enabled: boolean
queries: {
total: number
hubOnly: number
hybrid: number
onDemand: number
graph: number
}
latency: {
avg: string
p50: string
p95: string
p99: string
avgHub: string
avgRecompute: string
}
storage: {
totalDocuments: number
hubDocuments: number
storedEmbeddings: number
savingsPercent: number
recomputedTotal: number
}
cache: {
hits: number
misses: number
hitRate: number
}
graph: {
traversals: number
avgDepth: number
}
uptime: string
message?: string
}
+9 -1
View File
@@ -1,4 +1,4 @@
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats } from '@/types'
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats, GraphStats, VectorMetrics } from '@/types'
const API_BASE = '/api'
const DEFAULT_TIMEOUT = 10000 // 10 seconds
@@ -164,3 +164,11 @@ export async function fetchRelatedObservations(observationId: number, minConfide
export async function fetchRelationStats(signal?: AbortSignal): Promise<RelationStats> {
return fetchWithRetry<RelationStats>(`${API_BASE}/relations/stats`, { signal })
}
export async function fetchGraphStats(signal?: AbortSignal): Promise<GraphStats> {
return fetchWithRetry<GraphStats>(`${API_BASE}/graph/stats`, { signal })
}
export async function fetchVectorMetrics(signal?: AbortSignal): Promise<VectorMetrics> {
return fetchWithRetry<VectorMetrics>(`${API_BASE}/vector/metrics`, { signal })
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usegraphmetrics.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}