The search service can hold several Milvus collections, each with its own embedder, but a search names one or gets the default, so a second collection was invisible to the assistant. The client now learns the list off the query path (at warm-up and on the session timer), and only when the service lists more than the default does a search fan out — one call per collection in parallel, fused by reciprocal rank so scores from different embedders are never compared. With one collection, today's case, the request is byte-for- byte what it was and no listing call is made while anyone waits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
60 lines
3.6 KiB
JavaScript
60 lines
3.6 KiB
JavaScript
// A second search collection is searched alongside the first and fused by
|
|
// rank; with one collection the search is the single call it always was.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const fanout = require('../src/utils/collectionFanout');
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'src/utils/clinicalMcpClient.js'), 'utf8');
|
|
|
|
function listing(names, defaultName) {
|
|
return { structuredContent: { collections: names.map(n => ({ name: n, default: n === defaultName, model: 'm' })) } };
|
|
}
|
|
|
|
test('the default collection alone means nothing extra to search', () => {
|
|
assert.deepEqual(fanout.extraCollectionNames(listing(['mcp_bge_m3_1024'], 'mcp_bge_m3_1024')), []);
|
|
assert.deepEqual(fanout.extraCollectionNames({}), []);
|
|
assert.deepEqual(fanout.extraCollectionNames(null), []);
|
|
});
|
|
|
|
test('collections beyond the default are named, from either envelope', () => {
|
|
assert.deepEqual(fanout.extraCollectionNames(listing(['a', 'b', 'c'], 'a')), ['b', 'c']);
|
|
const textEnvelope = { content: [{ type: 'text', text: JSON.stringify({ collections: [{ name: 'a', default: true }, { name: 'peds' }] }) }] };
|
|
assert.deepEqual(fanout.extraCollectionNames(textEnvelope), ['peds']);
|
|
});
|
|
|
|
test('ranked lists from two collections fuse by reciprocal rank and keep their origin', () => {
|
|
const first = { structuredContent: { results: [{ id: 'x', score: 0.9 }, { id: 'y', score: 0.8 }], verified_chunk_count: 2 } };
|
|
const second = { structuredContent: { results: [{ id: 'z', score: 0.1 }, { id: 'y', score: 0.05 }], verified_chunk_count: 2, dropped_document_count: 1 } };
|
|
const merged = fanout.mergeCollectionResults([{ collection: null, response: first }, { collection: 'peds', response: second }], 10);
|
|
// y appears in both lists (ranks 2 and 2) and outranks either list's first.
|
|
assert.equal(merged.results[0].id, 'y');
|
|
assert.deepEqual(merged.results.map(r => r.id).sort(), ['x', 'y', 'z']);
|
|
assert.equal(merged.results.find(r => r.id === 'z').collection, 'peds');
|
|
assert.equal(merged.results.find(r => r.id === 'x').collection, null);
|
|
assert.equal(merged.verified_chunk_count, 4);
|
|
assert.equal(merged.dropped_document_count, 1);
|
|
assert.deepEqual(merged.collections, [null, 'peds']);
|
|
});
|
|
|
|
test('the fused list respects the limit and a failed collection is simply absent', () => {
|
|
const one = { structuredContent: { results: [{ id: 1 }, { id: 2 }, { id: 3 }] } };
|
|
const merged = fanout.mergeCollectionResults([{ collection: null, response: one }], 2);
|
|
assert.equal(merged.results.length, 2);
|
|
assert.deepEqual(merged.collections, [null]);
|
|
});
|
|
|
|
test('the search itself stays one call when no extra collection is known', () => {
|
|
// The single-call path is the first thing semanticSearch does, before any
|
|
// fan-out machinery, and it names no collection.
|
|
const body = src.slice(src.indexOf('async function semanticSearch'), src.indexOf('// The collections the service will search'));
|
|
assert.match(body, /if \(extra\.length === 0\) return callMcpTool\(SEARCH_TOOL_NAME, searchArgs\(query, opts\)\)/);
|
|
assert.match(src, /if \(collection\) args\.collection = collection;/);
|
|
});
|
|
|
|
test('the collection list is learned off the query path, never inside a search', () => {
|
|
const search = src.slice(src.indexOf('async function semanticSearch'), src.indexOf('// The collections the service will search'));
|
|
assert.doesNotMatch(search, /refreshCollections|clinical_list_collections/);
|
|
const warm = src.slice(src.indexOf('function warmMcpSession'), src.indexOf('function busyError'));
|
|
assert.match(warm, /if \(collectionsStale\(\)\) refreshCollections\(\);/);
|
|
});
|