Files
gbrain/test/rrf-source-key.test.ts
T
Garry TanandClaude Opus 4.8 78d2d3dc18 feat(search): relational recall arm + federation key hardening
Wires edge-derived candidates into bare hybridSearch as a FOURTH RRF arm
(relational-recall.ts): parse the original query -> scope-aware,
confidence-gated seed resolution (drops fallback_slugify; never traverses
from a guess) -> relationalFanout -> batch-hydrate, reinforcing each page's
REAL canonical chunk (page-level key for chunkless entity pages) ->
--explain attribution + fail-open audit row. Text-only (no-op in image
mode); pure no-op for non-relational queries; rides every downstream stage
(cosine, post-fusion boosts, dedup, reranker, autocut, token budget).

Mode wiring: relationalRetrieval + relational_retrieval_depth knobs
(conservative off; balanced/tokenmax on; depth 2), per-call thread-through
in both bare + cached paths, KNOBS_HASH_VERSION 9->10 (rel=/reld=), config
keys, modes-dashboard descriptions, and a `relational` param on the query op.

Federation hardening (structural, engine-wide): the RRF/dedup key now
carries source_id via a shared rrfKey() (fixes a latent cross-source
collapse where same-slug pages in different sources merged), and the query
cache scopes by a canonical source-set key (cacheScopeKey) so a federated
read can't be served a single-source row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:08:51 -07:00

71 lines
3.0 KiB
TypeScript

/**
* RRF fusion key is source-aware (federation hardening).
*
* Pre-fix the RRF/dedup key was `slug:chunk_id`, which collapsed two
* same-slug pages living in different federated sources into one fusion
* entry — a cross-source recall bug. The key is now
* `(source_id, slug, chunk_id)`, matching `dedup.ts:pageKey`'s composite
* discipline at chunk granularity. These tests pin that behavior for both
* `rrfFusion` and `rrfFusionWeighted` and confirm single-source ranking is
* unchanged.
*/
import { describe, test, expect } from 'bun:test';
import { rrfFusion, rrfFusionWeighted } from '../src/core/search/hybrid.ts';
import type { SearchResult } from '../src/core/types.ts';
function makeResult(overrides: Partial<SearchResult> = {}): SearchResult {
return {
slug: 'test-page',
page_id: 1,
title: 'Test',
type: 'concept',
chunk_text: 'unique chunk text',
chunk_source: 'timeline', // avoid compiled_truth 2x boost noise
chunk_id: 1,
chunk_index: 0,
score: 0.5,
stale: false,
...overrides,
};
}
describe('RRF key is source-aware', () => {
test('same slug + chunk_id in DIFFERENT sources do NOT collapse', () => {
const a = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-a', chunk_text: 'x' });
const b = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-b', chunk_text: 'y' });
const out = rrfFusion([[a, b]], 60);
expect(out.length).toBe(2);
expect(new Set(out.map(r => r.source_id))).toEqual(new Set(['team-a', 'team-b']));
});
test('same slug + chunk_id + SAME source DO collapse (reinforce across lists)', () => {
const a = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-a', chunk_text: 'x' });
const b = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-a', chunk_text: 'x' });
const out = rrfFusion([[a], [b]], 60);
expect(out.length).toBe(1);
});
test('single-source (no source_id → "default") ranking unchanged', () => {
const a = makeResult({ slug: 'a', chunk_id: 1, chunk_text: 'aaa' });
const b = makeResult({ slug: 'b', chunk_id: 2, chunk_text: 'bbb' });
const out = rrfFusion([[a, b]], 60);
expect(out.length).toBe(2);
expect(out[0].slug).toBe('a'); // rank-0 wins, as before the key change
});
test('chunkless rows (null chunk_id) stay distinct per source via text prefix', () => {
const a = makeResult({ slug: 'people/alice', chunk_id: undefined as unknown as number, source_id: 'team-a', chunk_text: 'alice page' });
const b = makeResult({ slug: 'people/alice', chunk_id: undefined as unknown as number, source_id: 'team-b', chunk_text: 'alice page' });
const out = rrfFusion([[a, b]], 60);
expect(out.length).toBe(2);
});
test('weighted RRF is also source-aware', () => {
const a = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-a' });
const b = makeResult({ slug: 'people/alice', chunk_id: 1, source_id: 'team-b' });
const out = rrfFusionWeighted([{ list: [a, b], k: 60 }]);
expect(out.length).toBe(2);
});
});