@@ -117,13 +117,20 @@ const PROJECTION_FILLED_TTL_MS = 60_000
117117 * Whether the ranking projection still holds rows the backfill has not filled. Read off the
118118 * unfilled-rows index in microseconds and remembered briefly: the answer only ever changes once.
119119 */
120- const projectionFilled = new LRUCache < ProjectionSourceAclTable , boolean > ( {
120+ const projectionFilled = new LRUCache <
121+ ProjectionSourceAclTable ,
122+ boolean ,
123+ { budget : SearchBudget | undefined ; stage : SearchStage }
124+ > ( {
121125 max : PROJECTION_SOURCE_ACL_TABLES . length ,
122126 ttl : PROJECTION_FILLED_TTL_MS ,
123- fetchMethod : async ( projection ) => {
127+ /** The read that misses the cache spends the leg's own budget, like every other read of the leg. */
128+ fetchMethod : async ( projection , _stale , { context } ) => {
124129 const table = projection === 'embedding_search' ? embeddingSearch : embeddingKeywordTin
125- const [ row ] = await db . execute < { unfilled : boolean } > ( sql `
130+ const [ row ] = await runSearchQuery ( context . budget , context . stage , ( executor ) =>
131+ executor . execute < { unfilled : boolean } > ( sql `
126132 SELECT EXISTS (SELECT 1 FROM ${ table } WHERE ${ table . acl } IS NULL) AS unfilled` )
133+ )
127134 return ! row ?. unfilled
128135 } ,
129136} )
@@ -1770,7 +1777,11 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
17701777 }
17711778 let selected : Array < { id : string } >
17721779 const plan = params . access . kind === 'user' ? params . accessPlan : undefined
1773- const filled = plan ? ( ( await projectionFilled . fetch ( 'embedding_search' ) ) ?? false ) : false
1780+ const filled = plan
1781+ ? ( ( await projectionFilled . fetch ( 'embedding_search' , {
1782+ context : { budget : params . budget , stage : 'vector.projection_filled' } ,
1783+ } ) ) ?? false )
1784+ : false
17741785 /**
17751786 * A source the caller is a member of that has its own index is walked on its own, which
17761787 * beats ranking it exactly once it is large enough to have earned that index.
@@ -1891,29 +1902,32 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
18911902 vectorCandidateScan : selected . length < candidateLimit ? 'underfilled' : 'planned' ,
18921903 } )
18931904 }
1894- const slice = candidatePool . ids . slice ( offset , offset + limit )
1895- if ( ! slice . length ) return { candidates : [ ] , nextOffset : offset }
18961905 /**
18971906 * The walk's order is the page's order: it ranked on the stored halfvec, and rescoring the
18981907 * pool against the original vectors read one out-of-line vector per candidate from storage
18991908 * no cache holds, seconds on a query nobody had run before. Only the page's identities are
1900- * read here; the full read predicate follows at hydration, as before.
1909+ * read here; the full read predicate follows at hydration, as before. A slice whose
1910+ * documents all went away since the walk is passed over, not mistaken for the pool's end.
19011911 */
1902- const ranked = new Map ( slice . map ( ( candidate , index ) => [ candidate . id , index ] ) )
1903- const identities = await runSearchQuery ( params . budget , 'vector.page' , ( executor ) =>
1904- executor . execute < SearchReadCandidate > ( sql `
1912+ for ( let start = offset ; start < candidatePool . ids . length ; start += limit ) {
1913+ const slice = candidatePool . ids . slice ( start , start + limit )
1914+ const ranked = new Map ( slice . map ( ( candidate , index ) => [ candidate . id , index ] ) )
1915+ const identities = await runSearchQuery ( params . budget , 'vector.page' , ( executor ) =>
1916+ executor . execute < SearchReadCandidate > ( sql `
19051917 SELECT ${ embeddingSearch . id } AS id, ${ document . id } AS "documentId",
19061918 ${ document . connectorId } AS "connectorId"
19071919 FROM ${ embeddingSearch }
19081920 INNER JOIN ${ document } ON ${ document . id } = ${ embeddingSearch . documentId }
19091921 WHERE ${ embeddingSearch . id } = ANY(${ textArrayLiteral ( slice . map ( ( candidate ) => candidate . id ) ) } )
19101922 ` )
1911- )
1912- const page = [ ...identities ] . sort ( ( a , b ) => ( ranked . get ( a . id ) ?? 0 ) - ( ranked . get ( b . id ) ?? 0 ) )
1913- return {
1914- candidates : page ,
1915- nextOffset : offset + page . length ,
1923+ )
1924+ if ( ! identities . length ) continue
1925+ const page = [ ...identities ] . sort (
1926+ ( a , b ) => ( ranked . get ( a . id ) ?? 0 ) - ( ranked . get ( b . id ) ?? 0 )
1927+ )
1928+ return { candidates : page , nextOffset : start + slice . length }
19161929 }
1930+ return { candidates : [ ] , nextOffset : candidatePool . ids . length }
19171931 } ,
19181932 hydrate : ( ids , authorized ) =>
19191933 hydrateSearchCandidates (
@@ -2027,7 +2041,9 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise
20272041 /** A filled projection decides readability on the ranked row alone; none of its rows needs the document. */
20282042 const tinFilled =
20292043 accessPlan && tinQuery
2030- ? ( ( await projectionFilled . fetch ( 'embedding_keyword_tin' ) ) ?? false )
2044+ ? ( ( await projectionFilled . fetch ( 'embedding_keyword_tin' , {
2045+ context : { budget : params . budget , stage : 'keyword.projection_filled' } ,
2046+ } ) ) ?? false )
20312047 : false
20322048 /** The projection predicate over the ranked CTE's mirrored columns, plus any excluded source. */
20332049 const onRowKeywordVisibility = ( excludedSources : readonly string [ ] ) =>
0 commit comments