fix: port search and dimension safety guardrails

- enforce canonical dimensions across app and MCP surfaces
- prioritize title-first node search in UI and query tools
- add regression tests for ranking and dimension validation
This commit is contained in:
“BeeRad”
2026-03-23 10:47:18 +11:00
parent c1eda905d3
commit 545dc6e2e8
18 changed files with 250 additions and 78 deletions
+18 -6
View File
@@ -2,6 +2,7 @@ import { getSQLiteClient } from './sqlite-client';
import { Node, NodeFilters } from '@/types/database';
import { eventBroadcaster } from '../events';
import { EmbeddingService } from '@/services/embeddings';
import { scoreNodeSearchMatch } from './searchRanking';
type NodeRow = Node & { dimensions_json: string };
type NodeSearchRow = NodeRow & { rank?: number; similarity?: number };
@@ -496,17 +497,23 @@ export class NodeService {
return [];
}
const searchLimit = Math.max(limit + offset, Math.min(limit * 5, 100));
let rankedRows = this.searchNodesFts(sqlite, search, filters, searchLimit);
const searchLimit = Math.max(limit + offset, Math.min(limit * 20, 250));
const candidateRows = new Map<number, NodeSearchRow>();
if (rankedRows.length === 0) {
rankedRows = this.searchNodesLike(sqlite, search, filters, searchLimit);
for (const row of this.searchNodesFts(sqlite, search, filters, searchLimit)) {
candidateRows.set(row.id, row);
}
if (rankedRows.length === 0) {
rankedRows = this.searchNodesLikeRelaxed(sqlite, search, filters, searchLimit);
for (const row of this.searchNodesLike(sqlite, search, filters, searchLimit)) {
if (!candidateRows.has(row.id)) candidateRows.set(row.id, row);
}
for (const row of this.searchNodesLikeRelaxed(sqlite, search, filters, searchLimit)) {
if (!candidateRows.has(row.id)) candidateRows.set(row.id, row);
}
let rankedRows = Array.from(candidateRows.values());
if ((filters.searchMode ?? 'standard') === 'hybrid') {
const vectorRows = await this.searchNodesVector(sqlite, search, filters, searchLimit);
if (vectorRows.length > 0) {
@@ -514,6 +521,11 @@ export class NodeService {
}
}
rankedRows = rankedRows
.map(row => ({ row, score: scoreNodeSearchMatch(row, search) }))
.sort((a, b) => b.score - a.score)
.map(entry => entry.row);
return rankedRows
.slice(offset, offset + limit)
.map(row => this.mapNodeRow(row));