Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion apps/api/src/rag/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ function getRagmapMeta(entry: RegistryServerEntry): any {
return (entry._meta?.[META_RAGMAP_KEY] as any) ?? {};
}

function remoteTypeWeight(value: unknown): number {
if (value === 'streamable-http') return 2;
if (value === 'sse') return 1;
return 0;
}

function checkedAtMs(value: unknown): number {
if (typeof value !== 'string' || !value) return Number.NEGATIVE_INFINITY;
const ts = Date.parse(value);
return Number.isFinite(ts) ? ts : Number.NEGATIVE_INFINITY;
}

function compareReachabilityPreference(
a: RegistryServerEntry,
b: RegistryServerEntry,
enabled: boolean
): number {
if (!enabled) return 0;
const ragA = getRagmapMeta(a);
const ragB = getRagmapMeta(b);

const typeWeightA = remoteTypeWeight(ragA?.reachableRemoteType);
const typeWeightB = remoteTypeWeight(ragB?.reachableRemoteType);
if (typeWeightA !== typeWeightB) return typeWeightB - typeWeightA;

const checkedAtA = checkedAtMs(ragA?.reachableCheckedAt ?? ragA?.lastReachableAt);
const checkedAtB = checkedAtMs(ragB?.reachableCheckedAt ?? ragB?.lastReachableAt);
if (checkedAtA !== checkedAtB) return checkedAtB - checkedAtA;

return 0;
}

function compareQualitySignals(a: RagSearchHit, b: RagSearchHit): number {
const ragA = getRagmapMeta(a.entry);
const ragB = getRagmapMeta(b.entry);
Expand Down Expand Up @@ -221,6 +253,7 @@ export function ragSearchKeyword(
limit: number,
filters?: RagFilters
): RagSearchHit[] {
const preferReachabilitySignals = filters?.reachable === true;
const tokens = tokenize(query);
// Match tokens at word boundaries (prefix match) so "rag" doesn't match "storage".
const tokenRegexes = tokens.map((t) => new RegExp(`\\b${escapeRegExp(t)}`, 'i'));
Expand All @@ -233,6 +266,8 @@ export function ragSearchKeyword(
}
scored.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
const reachabilityPreference = compareReachabilityPreference(a.entry, b.entry, preferReachabilitySignals);
if (reachabilityPreference !== 0) return reachabilityPreference;
return compareQualitySignals(a, b);
});
return scored.slice(0, limit);
Expand All @@ -244,6 +279,7 @@ export function ragSearchSemantic(
limit: number,
filters?: RagFilters
): RagSearchHit[] {
const preferReachabilitySignals = filters?.reachable === true;
const scored: RagSearchHit[] = [];
for (const item of items) {
if (!passesFilters(item, filters)) continue;
Expand All @@ -255,6 +291,8 @@ export function ragSearchSemantic(
}
scored.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
const reachabilityPreference = compareReachabilityPreference(a.entry, b.entry, preferReachabilitySignals);
if (reachabilityPreference !== 0) return reachabilityPreference;
return compareQualitySignals(a, b);
});
return scored.slice(0, limit);
Expand All @@ -265,12 +303,17 @@ export function ragSearchTop(
limit: number,
filters?: RagFilters
): RagSearchHit[] {
const preferReachabilitySignals = filters?.reachable === true;
const scored: RagSearchHit[] = [];
for (const item of items) {
if (!passesFilters(item, filters)) continue;
const score = Number(item.enrichment?.ragScore ?? 0);
scored.push({ entry: item.entry, kind: 'keyword', score });
}
scored.sort(compareQualitySignals);
scored.sort((a, b) => {
const reachabilityPreference = compareReachabilityPreference(a.entry, b.entry, preferReachabilitySignals);
if (reachabilityPreference !== 0) return reachabilityPreference;
return compareQualitySignals(a, b);
});
return scored.slice(0, limit);
}
136 changes: 136 additions & 0 deletions apps/api/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,142 @@ test('rag top applies reachableMaxAgeHours when reachable=true and echoes the fi
await app.close();
});

test('rag top prefers reachable streamable-http over sse when reachable=true', async () => {
const store = new InMemoryStore();
const nowMs = Date.now();
const olderCheckedAt = new Date(nowMs - 2 * 3_600_000).toISOString();
const newerCheckedAt = new Date(nowMs - 30 * 60_000).toISOString();
const officialUpdatedAt = '2026-03-01T00:00:00.000Z';

await store.upsertServerVersion({
runId: 'run_test',
at: new Date(),
server: {
name: 'example/top-streamable-older',
version: '0.1.0',
description: 'retrieval rag server',
remotes: [{ type: 'streamable-http', url: 'https://example.com/top-streamable' }]
},
official: { isLatest: true, updatedAt: officialUpdatedAt, publishedAt: officialUpdatedAt },
ragmap: {
categories: ['rag'],
ragScore: 70,
reasons: ['test'],
keywords: ['rank'],
serverKind: 'retriever',
hasRemote: true,
reachable: true,
reachableRemoteType: 'streamable-http',
reachableCheckedAt: olderCheckedAt
},
hidden: false
});
await store.upsertServerVersion({
runId: 'run_test',
at: new Date(),
server: {
name: 'example/top-sse-newer',
version: '0.1.0',
description: 'retrieval rag server',
remotes: [{ type: 'sse', url: 'https://example.com/top-sse' }]
},
official: { isLatest: true, updatedAt: officialUpdatedAt, publishedAt: officialUpdatedAt },
ragmap: {
categories: ['rag'],
ragScore: 70,
reasons: ['test'],
keywords: ['rank'],
serverKind: 'retriever',
hasRemote: true,
reachable: true,
reachableRemoteType: 'sse',
reachableCheckedAt: newerCheckedAt
},
hidden: false
});

const app = await buildApp({ env, store });
const res = await app.inject({
method: 'GET',
url: '/rag/top?reachable=true&reachableMaxAgeHours=24&minScore=0&serverKind=retriever&limit=10'
});
assert.equal(res.statusCode, 200);
const body = res.json() as any;
assert.equal(body.metadata.count, 2);
assert.equal(body.results[0].name, 'example/top-streamable-older');
assert.equal(body.results[1].name, 'example/top-sse-newer');

await app.close();
});

test('rag search prefers more recently checked entries when remote type ties', async () => {
const store = new InMemoryStore();
const nowMs = Date.now();
const olderCheckedAt = new Date(nowMs - 5 * 3_600_000).toISOString();
const newerCheckedAt = new Date(nowMs - 45 * 60_000).toISOString();
const officialUpdatedAt = '2026-03-01T00:00:00.000Z';

await store.upsertServerVersion({
runId: 'run_test',
at: new Date(),
server: {
name: 'example/search-streamable-older',
version: '0.1.0',
description: 'ranksignal retriever endpoint',
remotes: [{ type: 'streamable-http', url: 'https://example.com/search-older' }]
},
official: { isLatest: true, updatedAt: officialUpdatedAt, publishedAt: officialUpdatedAt },
ragmap: {
categories: ['rag'],
ragScore: 65,
reasons: ['test'],
keywords: ['ranksignal'],
serverKind: 'retriever',
hasRemote: true,
reachable: true,
reachableRemoteType: 'streamable-http',
reachableCheckedAt: olderCheckedAt
},
hidden: false
});
await store.upsertServerVersion({
runId: 'run_test',
at: new Date(),
server: {
name: 'example/search-streamable-newer',
version: '0.1.0',
description: 'ranksignal retriever endpoint',
remotes: [{ type: 'streamable-http', url: 'https://example.com/search-newer' }]
},
official: { isLatest: true, updatedAt: officialUpdatedAt, publishedAt: officialUpdatedAt },
ragmap: {
categories: ['rag'],
ragScore: 65,
reasons: ['test'],
keywords: ['ranksignal'],
serverKind: 'retriever',
hasRemote: true,
reachable: true,
reachableRemoteType: 'streamable-http',
reachableCheckedAt: newerCheckedAt
},
hidden: false
});

const app = await buildApp({ env, store });
const res = await app.inject({
method: 'GET',
url: '/rag/search?q=ranksignal&hasRemote=true&reachable=true&reachableMaxAgeHours=24&limit=10'
});
assert.equal(res.statusCode, 200);
const body = res.json() as any;
assert.equal(body.metadata.count, 2);
assert.equal(body.results[0].name, 'example/search-streamable-newer');
assert.equal(body.results[1].name, 'example/search-streamable-older');

await app.close();
});

test('rag install returns copy-ready config object', async () => {
const store = new InMemoryStore();
await store.upsertServerVersion({
Expand Down