Skip to content

Commit c8f35db

Browse files
jonathanMLDevzho
andauthored
fixed reassemble and query format module (#96)
* fixed reassemble and query format module * fixed format errors * addressed coderabbitai review * fixed format error * added test --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent 4651682 commit c8f35db

4 files changed

Lines changed: 210 additions & 12 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import * as loggerModule from '../logger.js';
3+
import type { SearchResult } from '../types.js';
4+
import {
5+
formatQueryResultRows,
6+
formatSearchResultAsRow,
7+
resetPaperNumberDeprecationLatchForTests,
8+
} from './format-query-result.js';
9+
10+
const DEPRECATION_SUBSTRING =
11+
'paper_number is deprecated and will be removed in the next major release';
12+
13+
describe('formatSearchResultAsRow / formatQueryResultRows', () => {
14+
let warnSpy: ReturnType<typeof vi.spyOn>;
15+
16+
beforeEach(() => {
17+
resetPaperNumberDeprecationLatchForTests();
18+
warnSpy = vi.spyOn(loggerModule, 'warn').mockImplementation(() => {});
19+
});
20+
21+
afterEach(() => {
22+
warnSpy.mockRestore();
23+
});
24+
25+
it('warns once on first row even when document_id is null', () => {
26+
const doc: SearchResult = {
27+
id: 'v1',
28+
content: 'hello world',
29+
score: 0.99,
30+
metadata: { title: 'T' },
31+
reranked: false,
32+
};
33+
const row = formatSearchResultAsRow(doc);
34+
expect(row.document_id).toBeNull();
35+
expect(warnSpy).toHaveBeenCalledTimes(1);
36+
expect(String(warnSpy.mock.calls[0]?.[0])).toContain(DEPRECATION_SUBSTRING);
37+
});
38+
39+
it('does not warn again on second formatSearchResultAsRow call', () => {
40+
const doc: SearchResult = {
41+
id: 'v1',
42+
content: 'a',
43+
score: 0.5,
44+
metadata: { document_number: 'P1' },
45+
reranked: false,
46+
};
47+
formatSearchResultAsRow(doc);
48+
formatSearchResultAsRow(doc);
49+
expect(warnSpy).toHaveBeenCalledTimes(1);
50+
});
51+
52+
it('warns at most once for formatQueryResultRows with multiple hits', () => {
53+
const results: SearchResult[] = [
54+
{
55+
id: 'a',
56+
content: 'one',
57+
score: 0.9,
58+
metadata: { document_number: 'D1' },
59+
reranked: false,
60+
},
61+
{
62+
id: 'b',
63+
content: 'two',
64+
score: 0.8,
65+
metadata: { document_number: 'D2' },
66+
reranked: false,
67+
},
68+
];
69+
formatQueryResultRows(results);
70+
expect(warnSpy).toHaveBeenCalledTimes(1);
71+
});
72+
});

src/server/format-query-result.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export interface QueryResultRow {
2121
document_id: string | null;
2222
/**
2323
* @deprecated Use `document_id`. Kept for one minor cycle and removed in
24-
* the next major release. The first time a row is emitted with this alias
25-
* during a session, a `WARN` log is fired so consumers see the deadline.
24+
* the next major release. The first formatted query-result row in the
25+
* process triggers a single `WARN` log per session so consumers see the
26+
* deprecation deadline.
2627
*/
2728
paper_number: string | null;
2829
title: string;
@@ -79,7 +80,7 @@ export function formatSearchResultAsRow(
7980
: null) ??
8081
null;
8182

82-
if (document_id !== null && !deprecationWarnedThisSession) {
83+
if (!deprecationWarnedThisSession) {
8384
deprecationWarnedThisSession = true;
8485
logWarn(
8586
'paper_number is deprecated and will be removed in the next major release; use document_id instead.'

src/server/reassemble-documents.test.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import * as loggerModule from '../logger.js';
23
import { reassembleByDocument } from './reassemble-documents.js';
34
import type { SearchResult } from '../types.js';
45

56
describe('reassembleByDocument', () => {
7+
let warnSpy: ReturnType<typeof vi.spyOn>;
8+
9+
beforeEach(() => {
10+
warnSpy = vi.spyOn(loggerModule, 'warn').mockImplementation(() => {});
11+
});
12+
13+
afterEach(() => {
14+
warnSpy.mockRestore();
15+
});
16+
617
it('groups chunks by document_number', () => {
718
const results: SearchResult[] = [
819
{
@@ -85,4 +96,102 @@ describe('reassembleByDocument', () => {
8596
expect(docs[0].chunk_count).toBe(3);
8697
expect(docs[0].merged_content).toBe('Chunk 0\n\nChunk 1\n\nChunk 2');
8798
});
99+
100+
it('does not warn when all hits have a document key', () => {
101+
const results: SearchResult[] = [
102+
{
103+
id: 'c1',
104+
content: 'Only doc.',
105+
score: 0.9,
106+
metadata: { document_number: 'P1' },
107+
reranked: false,
108+
},
109+
];
110+
reassembleByDocument(results);
111+
expect(warnSpy).not.toHaveBeenCalled();
112+
});
113+
114+
it('warns once with count when hits lack document key and empty vector id', () => {
115+
const results: SearchResult[] = [
116+
{
117+
id: '',
118+
content: 'Orphan chunk.',
119+
score: 0.5,
120+
metadata: {},
121+
reranked: false,
122+
},
123+
];
124+
const docs = reassembleByDocument(results);
125+
expect(docs).toHaveLength(0);
126+
expect(warnSpy).toHaveBeenCalledTimes(1);
127+
expect(warnSpy.mock.calls[0]?.[0]).toMatch(/skipped 1 hit/);
128+
expect(warnSpy.mock.calls[0]?.[0]).toMatch(/sample_ids=<empty>/);
129+
});
130+
131+
it('aggregates multiple skipped hits into one warn with total count', () => {
132+
const results: SearchResult[] = [
133+
{
134+
id: '',
135+
content: 'A',
136+
score: 0.5,
137+
metadata: {},
138+
reranked: false,
139+
},
140+
{
141+
id: '',
142+
content: 'B',
143+
score: 0.4,
144+
metadata: {},
145+
reranked: false,
146+
},
147+
];
148+
const docs = reassembleByDocument(results);
149+
expect(docs).toHaveLength(0);
150+
expect(warnSpy).toHaveBeenCalledTimes(1);
151+
expect(warnSpy.mock.calls[0]?.[0]).toMatch(/skipped 2 hit/);
152+
});
153+
154+
it('warns only for invalid hits when mixed with valid document keys', () => {
155+
const results: SearchResult[] = [
156+
{
157+
id: '',
158+
content: 'Skipped.',
159+
score: 0.3,
160+
metadata: {},
161+
reranked: false,
162+
},
163+
{
164+
id: 'vec-1',
165+
content: 'Kept by id.',
166+
score: 0.9,
167+
metadata: {},
168+
reranked: false,
169+
},
170+
];
171+
const docs = reassembleByDocument(results);
172+
expect(docs).toHaveLength(1);
173+
expect(docs[0].document_id).toBe('vec-1');
174+
expect(warnSpy).toHaveBeenCalledTimes(1);
175+
expect(warnSpy.mock.calls[0]?.[0]).toMatch(/skipped 1 hit/);
176+
expect(warnSpy.mock.calls[0]?.[0]).toMatch(/sample_ids=<empty>/);
177+
});
178+
179+
it('includes at most three sample entries in skip warning when many hits are skipped', () => {
180+
const results: SearchResult[] = Array.from({ length: 4 }, (_, i) => ({
181+
id: '',
182+
content: `chunk-${i}`,
183+
score: 0.1 * (i + 1),
184+
metadata: {},
185+
reranked: false,
186+
}));
187+
reassembleByDocument(results);
188+
expect(warnSpy).toHaveBeenCalledTimes(1);
189+
const msg = String(warnSpy.mock.calls[0]?.[0]);
190+
expect(msg).toMatch(/skipped 4 hit/);
191+
const samplePart = msg.match(/sample_ids=(.+)$/)?.[1];
192+
expect(samplePart).toBeDefined();
193+
const samples = samplePart!.split(',');
194+
expect(samples).toHaveLength(3);
195+
expect(samples.every((s) => s === '<empty>')).toBe(true);
196+
});
88197
});

src/server/reassemble-documents.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,24 @@
55
*/
66

77
import type { PineconeMetadataValue, SearchResult } from '../types.js';
8+
import { warn as logWarn } from '../logger.js';
89

910
/** Default metadata keys tried for chunk ordering (RecursiveCharacterTextSplitter often adds these). */
1011
const CHUNK_ORDER_KEYS = ['chunk_index', 'start_index', 'loc'] as const;
1112

13+
/** Max vector ids listed in the one-per-call skip warning (for grep-friendly debugging). */
14+
const SKIP_WARN_SAMPLE_IDS = 3;
15+
1216
/** Derive a stable document key from hit metadata: document_number → url → doc_id → hit.id. */
1317
function getDocumentKey(hit: SearchResult): string {
1418
const m = hit.metadata || {};
1519
const docNumber = m['document_number'];
1620
const url = m['url'];
1721
const docId = m['doc_id'];
18-
return (
19-
(typeof docNumber === 'string' ? docNumber : undefined) ??
20-
(typeof url === 'string' ? url : undefined) ??
21-
(typeof docId === 'string' ? docId : undefined) ??
22-
hit.id ??
23-
''
24-
);
22+
const nonEmpty = (v: unknown): string | undefined =>
23+
typeof v === 'string' && v.trim() !== '' ? v : undefined;
24+
25+
return nonEmpty(docNumber) ?? nonEmpty(url) ?? nonEmpty(docId) ?? nonEmpty(hit.id) ?? '';
2526
}
2627

2728
/** Return a numeric order for sorting chunks from metadata (chunk_index, start_index, or loc). */
@@ -68,10 +69,18 @@ export function reassembleByDocument(
6869
const separator = options?.contentSeparator ?? '\n\n';
6970

7071
const byDoc = new Map<string, SearchResult[]>();
72+
let skippedHits = 0;
73+
const sampleIdsForWarn: string[] = [];
7174

7275
for (const hit of results) {
7376
const key = getDocumentKey(hit);
74-
if (!key) continue;
77+
if (!key) {
78+
skippedHits++;
79+
if (sampleIdsForWarn.length < SKIP_WARN_SAMPLE_IDS) {
80+
sampleIdsForWarn.push(hit.id.trim() === '' ? '<empty>' : hit.id);
81+
}
82+
continue;
83+
}
7584
let list = byDoc.get(key);
7685
if (!list) {
7786
list = [];
@@ -80,6 +89,13 @@ export function reassembleByDocument(
8089
list.push(hit);
8190
}
8291

92+
if (skippedHits > 0) {
93+
const sample = sampleIdsForWarn.length > 0 ? ` sample_ids=${sampleIdsForWarn.join(',')}` : '';
94+
logWarn(
95+
`reassembleByDocument: skipped ${skippedHits} hit(s) with no document key (document_number, url, doc_id, or non-empty vector id).${sample}`
96+
);
97+
}
98+
8399
const out: ReassembledDocument[] = [];
84100

85101
for (const [docId, chunks] of byDoc) {

0 commit comments

Comments
 (0)