Skip to content

Commit d534145

Browse files
committed
improvement(knowledge): widen a search to 50 chunks only when the reader asks for more
1 parent 09a7639 commit d534145

7 files changed

Lines changed: 62 additions & 32 deletions

File tree

‎apps/sim/app/o/[organizationId]/search/search.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ async function editDraft(value: string) {
135135
function expectVisibleQuery(query: string) {
136136
expect(searchInput().value).toBe(query)
137137
expect(container.querySelector('a[data-source-link]')?.textContent).toBe(`${query} launch plan`)
138-
expect(mocks.search).toHaveBeenLastCalledWith(scope, query, {})
138+
expect(mocks.search).toHaveBeenLastCalledWith(scope, query, {}, 20)
139139
expect(document.activeElement).toBe(searchInput())
140140
}
141141

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.test.tsx‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,12 @@ describe('result paging and the custom window', () => {
167167
similarity: 0.5,
168168
})
169169

170-
it('shows ten documents at a time and reveals more on request', async () => {
170+
it('offers more only after a full first page, and asks for the wider search on request', async () => {
171171
mocks.overview.mockReturnValue({ data: { providers: [], hasSearchableDocuments: true } })
172-
mocks.search.mockReturnValue({
172+
const page = (length: number) => ({
173173
data: {
174174
query: 'launch',
175-
results: Array.from({ length: 25 }, (_, n) => result(n)),
175+
results: Array.from({ length }, (_, n) => result(n)),
176176
retrieval: { status: 'complete', timedOutLegs: [] },
177177
},
178178
isPending: false,
@@ -181,20 +181,31 @@ describe('result paging and the custom window', () => {
181181
isError: false,
182182
refetch: mocks.retry,
183183
})
184+
mocks.search.mockReturnValue(page(20))
185+
await render()
186+
expect(mocks.search.mock.calls.at(-1)![3]).toBe(20)
187+
const more = () =>
188+
[...container.querySelectorAll('button')].find((b) => b.textContent === 'Show more')
189+
expect(more()).toBeDefined()
190+
await act(async () => more()!.click())
191+
/** The wider search is its own request; the first paint was never widened. */
192+
expect(mocks.search.mock.calls.at(-1)![3]).toBe(50)
193+
expect(more()).toBeUndefined()
194+
mocks.search.mockReturnValue(page(7))
184195
await render()
185-
expect(container.textContent).toContain('Document 9')
186-
expect(container.textContent).not.toContain('Document 10')
187-
const more = [...container.querySelectorAll('button')].find(
188-
(b) => b.textContent === 'Show more'
189-
)!
190-
expect(more).toBeDefined()
191-
await act(async () => more.click())
192-
expect(container.textContent).toContain('Document 19')
193-
expect(container.textContent).not.toContain('Document 20')
196+
expect(more()).toBeUndefined()
194197
})
195198

196199
it('searches a custom window as an inclusive range of days', async () => {
197200
mocks.overview.mockReturnValue({ data: { providers: [], hasSearchableDocuments: true } })
201+
mocks.search.mockReturnValue({
202+
data: { query: 'launch', results: [], retrieval: { status: 'complete', timedOutLegs: [] } },
203+
isPending: false,
204+
isFetching: false,
205+
isPlaceholderData: false,
206+
isError: false,
207+
refetch: mocks.retry,
208+
})
198209
await render(undefined, '?updated=custom&from=2026-09-01&to=2026-09-10')
199210
const filters = mocks.search.mock.calls.at(-1)![2]
200211
expect(filters.modifiedAfter).toBe('2026-09-01T00:00:00.000Z')

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { useState } from 'react'
44
import { Chip, ChipDatePicker, ChipLink, cn } from '@sim/emcn'
55
import { useQueryStates } from 'nuqs'
66
import { ActivityStatus } from '@/components/ui/activity-status'
7-
import type {
8-
WorkspaceKnowledgeSearchResult,
9-
WorkspaceSearchFilters,
7+
import {
8+
WORKSPACE_KNOWLEDGE_SEARCH_LIMITS,
9+
type WorkspaceKnowledgeSearchResult,
10+
type WorkspaceSearchFilters,
1011
} from '@/lib/api/contracts/knowledge'
1112
import { useSession } from '@/lib/auth/auth-client'
1213
import { type ResourceScope, resourceScopeKey } from '@/lib/core/resource-scope'
@@ -27,8 +28,6 @@ import { useSearchIndex, useSearchSourceOverview } from '@/hooks/queries/kb/conn
2728
import { useWorkspaceKnowledgeSearch } from '@/hooks/queries/kb/knowledge'
2829

2930
const DAY_MS = 24 * 60 * 60 * 1000
30-
/** Cards shown before the reader asks for more; the search itself returns several pages' worth. */
31-
const RESULTS_PAGE_SIZE = 10
3231
/** Every result without a connector is an upload; the filter names them so. */
3332
const UPLOAD_SOURCE = 'upload'
3433

@@ -129,7 +128,8 @@ interface SearchResultsProps {
129128
function SearchResults({ scope, query, onSummarize }: SearchResultsProps) {
130129
const [hasShownFilters, setHasShownFilters] = useState(false)
131130
const [searchedAt] = useState(Date.now)
132-
const [shown, setShown] = useState(RESULTS_PAGE_SIZE)
131+
/** More results are a second, wider search: the first paint stays as quick as it is. */
132+
const [expanded, setExpanded] = useState(false)
133133
const {
134134
data: index,
135135
isPending: basesPending,
@@ -158,7 +158,17 @@ function SearchResults({ scope, query, onSummarize }: SearchResultsProps) {
158158
isPlaceholderData,
159159
isError: searchFailed,
160160
refetch: refetchSearch,
161-
} = useWorkspaceKnowledgeSearch(scope, query, searchFilters)
161+
} = useWorkspaceKnowledgeSearch(
162+
scope,
163+
query,
164+
searchFilters,
165+
expanded
166+
? WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.expanded
167+
: WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.initial
168+
)
169+
/** A full first page may collapse to few cards, yet more documents may still match. */
170+
const mayHaveMore =
171+
!expanded && (search?.results.length ?? 0) >= WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.initial
162172
const { data: overview } = useSearchSourceOverview(scope)
163173
const indexing = (overview?.providers ?? [])
164174
.filter((provider) => provider.isSyncing)
@@ -292,7 +302,7 @@ function SearchResults({ scope, query, onSummarize }: SearchResultsProps) {
292302
className={cn('flex flex-col', isPlaceholderData && 'opacity-60')}
293303
onKeyDown={handleResultsKeyDown}
294304
>
295-
{documents.slice(0, shown).map((result) => {
305+
{documents.map((result) => {
296306
const source = toSource(result, query, scope)
297307
return (
298308
<SourceCard
@@ -311,9 +321,9 @@ function SearchResults({ scope, query, onSummarize }: SearchResultsProps) {
311321
/>
312322
)
313323
})}
314-
{documents.length > shown && (
324+
{mayHaveMore && (
315325
<div className='flex px-2 py-2'>
316-
<Chip variant='border' onClick={() => setShown((count) => count + RESULTS_PAGE_SIZE)}>
326+
<Chip variant='border' disabled={isFetching} onClick={() => setExpanded(true)}>
317327
Show more
318328
</Chip>
319329
</div>

‎apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-transitions.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe('search refinement with the real query cache and URL state', () => {
212212
expect(requests.at(-1)?.body).toEqual({
213213
organizationId: 'organization',
214214
query: 'launch',
215-
topK: 50,
215+
topK: 20,
216216
filters: expectedFilters,
217217
})
218218
expect(container.querySelector('h1')).toBeNull()

‎apps/sim/hooks/queries/kb/knowledge.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,11 @@ describe('knowledge query placeholder scope', () => {
214214
const query = captureQuery(() =>
215215
useWorkspaceKnowledgeSearch('workspace-1', 'new query', { source: 'slack' })
216216
)
217-
expect(query.queryKey).toEqual(
218-
knowledgeKeys.search('workspace-1', 'new query', { source: 'slack' }, 'reader')
219-
)
217+
/** The limit is the key's last part, so the wider search never evicts the first paint. */
218+
expect(query.queryKey).toEqual([
219+
...knowledgeKeys.search('workspace-1', 'new query', { source: 'slack' }, 'reader'),
220+
20,
221+
])
220222
expect(knowledgeKeys.search('workspace-1', 'query', { source: 'slack' })).not.toEqual(
221223
knowledgeKeys.search('workspace-1', 'query', { source: 'gitlab' })
222224
)

‎apps/sim/hooks/queries/kb/knowledge.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ import {
5151
updateKnowledgeChunkContract,
5252
updateKnowledgeDocumentContract,
5353
updateKnowledgeDocumentTagsContract,
54+
WORKSPACE_KNOWLEDGE_SEARCH_LIMITS,
5455
type WorkspaceKnowledgeSearchBody,
5556
type WorkspaceKnowledgeSearchData,
57+
type WorkspaceKnowledgeSearchLimit,
5658
} from '@/lib/api/contracts/knowledge'
5759
import type { WorkspaceSearchFilters } from '@/lib/api/contracts/knowledge/search'
5860
import { useSession } from '@/lib/auth/auth-client'
@@ -89,8 +91,6 @@ export const KNOWLEDGE_DOCUMENT_LIST_STALE_TIME = 60 * 1000
8991
export const KNOWLEDGE_CHUNK_LIST_STALE_TIME = 60 * 1000
9092
export const KNOWLEDGE_CHUNK_SEARCH_STALE_TIME = 60 * 1000
9193
export const WORKSPACE_KNOWLEDGE_SEARCH_STALE_TIME = 60 * 1000
92-
/** Chunks one search asks for: several pages of documents once collapsed to one card each. */
93-
export const WORKSPACE_KNOWLEDGE_SEARCH_RESULT_LIMIT = 50
9494
export const KNOWLEDGE_TAG_DEFINITION_LIST_STALE_TIME = 60 * 1000
9595
export const KNOWLEDGE_TAG_USAGE_STALE_TIME = 60 * 1000
9696
export const KNOWLEDGE_DOCUMENT_TAG_DEFINITION_LIST_STALE_TIME = 60 * 1000
@@ -1209,7 +1209,8 @@ async function searchWorkspaceKnowledge(
12091209
export function useWorkspaceKnowledgeSearch(
12101210
owner: string | ResourceScope | undefined,
12111211
query: string,
1212-
filters?: WorkspaceSearchFilters
1212+
filters?: WorkspaceSearchFilters,
1213+
limit: WorkspaceKnowledgeSearchLimit = WORKSPACE_KNOWLEDGE_SEARCH_LIMITS.initial
12131214
) {
12141215
const { data: session } = useSession()
12151216
const queryClient = useQueryClient()
@@ -1224,14 +1225,15 @@ export function useWorkspaceKnowledgeSearch(
12241225
const scopeKey =
12251226
scope?.kind === 'workspace' ? scope.workspaceId : scope ? resourceScopeKey(scope) : undefined
12261227
return useQuery({
1227-
queryKey: knowledgeKeys.search(scopeKey, trimmed, filters, userId),
1228+
/** The limit is the key's last part, so asking for more never evicts the first paint. */
1229+
queryKey: [...knowledgeKeys.search(scopeKey, trimmed, filters, userId), limit],
12281230
queryFn: ({ signal }) =>
12291231
searchWorkspaceKnowledge(
12301232
{
12311233
...(scope ? resourceScopeFields(scope) : {}),
12321234
query: trimmed,
12331235
filters,
1234-
topK: WORKSPACE_KNOWLEDGE_SEARCH_RESULT_LIMIT,
1236+
topK: limit,
12351237
},
12361238
signal
12371239
),

‎apps/sim/lib/api/contracts/knowledge/search.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ export const workspaceSearchFiltersSchema = z
194194
})
195195
export type WorkspaceSearchFilters = z.output<typeof workspaceSearchFiltersSchema>
196196

197+
/** Chunks a search asks for at first paint, and once the reader asks for more; both within `topK`'s bound. */
198+
export const WORKSPACE_KNOWLEDGE_SEARCH_LIMITS = { initial: 20, expanded: 50 } as const
199+
export type WorkspaceKnowledgeSearchLimit =
200+
(typeof WORKSPACE_KNOWLEDGE_SEARCH_LIMITS)[keyof typeof WORKSPACE_KNOWLEDGE_SEARCH_LIMITS]
201+
197202
export const workspaceKnowledgeSearchBodySchema = resourceOwnerSchema.safeExtend({
198203
filters: workspaceSearchFiltersSchema.optional(),
199204
query: z.string().trim().min(1, 'A search query is required').max(2000, 'Query is too long'),

0 commit comments

Comments
 (0)