diff --git a/src/components/Collections/SearchQuality/SearchQualityPanel.jsx b/src/components/Collections/SearchQuality/SearchQualityPanel.jsx index 9c2e56829..13376a316 100644 --- a/src/components/Collections/SearchQuality/SearchQualityPanel.jsx +++ b/src/components/Collections/SearchQuality/SearchQualityPanel.jsx @@ -76,6 +76,11 @@ VectorTableRow.propTypes = { }; const SAMPLE_SIZE = 100; +// Over-fetch so all-zero-vector points (e.g. bookkeeping markers) can be dropped +// and still leave SAMPLE_SIZE usable points. All-zero vectors score 0.0 against +// everything, corrupting the recall average. +const OVERFETCH_FACTOR = 2; +const isZeroVector = (vec) => Array.isArray(vec) && vec.length > 0 && vec.every((v) => v === 0); const SearchQualityPanel = ({ collectionName, vectors, loggingFoo, clearLogsFoo, ...other }) => { const { client } = useClient(); @@ -180,11 +185,12 @@ const SearchQualityPanel = ({ collectionName, vectors, loggingFoo, clearLogsFoo, try { const sampleResult = await client.scroll(collectionName, { with_payload: false, - with_vector: false, - limit: SAMPLE_SIZE, + with_vector: true, + limit: SAMPLE_SIZE * OVERFETCH_FACTOR, }); - const pointIds = sampleResult.points.map((point) => point.id); + const usablePoints = sampleResult.points.filter((p) => !isZeroVector(p.vector)); + const pointIds = usablePoints.slice(0, SAMPLE_SIZE).map((point) => point.id); const total = pointIds.length; loggingFoo && diff --git a/src/components/Collections/SearchQuality/searchQualityPannel.test.jsx b/src/components/Collections/SearchQuality/searchQualityPannel.test.jsx index c90444e12..55c7617eb 100644 --- a/src/components/Collections/SearchQuality/searchQualityPannel.test.jsx +++ b/src/components/Collections/SearchQuality/searchQualityPannel.test.jsx @@ -84,6 +84,33 @@ describe('SearchQualityPannel', () => { }); }); + it('should exclude all-zero-vector points from the sample', async () => { + const scrollMock = vi.fn().mockResolvedValue({ + points: [ + { id: 'zero-marker', vector: [0, 0, 0] }, + { id: 'normal-1', vector: [0.1, 0.2, 0.3] }, + ], + }); + const queryPointsMock = vi.fn().mockResolvedValue({ + data: { result: { points: [{ id: 1 }, { id: 2 }] }, status: 'ok', time: 0.005 }, + }); + useClient.mockReturnValue({ + client: { scroll: scrollMock, api: vi.fn().mockReturnValue({ queryPoints: queryPointsMock }) }, + }); + + render( + + + + ); + fireEvent.click(screen.getAllByTestId('index-quality-check-button')[0]); + + await waitFor(() => expect(queryPointsMock).toHaveBeenCalled()); + const queriedIds = queryPointsMock.mock.calls.map(([args]) => args.query); + expect(queriedIds).not.toContain('zero-marker'); + expect(queriedIds).toContain('normal-1'); + }); + it('should toggle advanced mode', () => { render(