@@ -216,6 +216,16 @@ export const VECTOR_PROBE_DOCUMENT_LIMIT = Math.round(
216216 ( VECTOR_PROBE_BUDGET_MS * 1000 ) / VECTOR_PROBE_MICROSECONDS_PER_DOCUMENT
217217)
218218
219+ /**
220+ * Documents a bounded permitted set may hold before ranking it exactly costs more than walking
221+ * the graph on the row. Exact ranking reads every chunk of the set, a few per document, where an
222+ * on-row walk reads at most {@link CANDIDATE_HNSW_MAX_SCAN_TUPLES} tuples; at this size the two
223+ * meet. A set past it is walked first and ranked exactly only if the walk cannot fill its pool, so
224+ * its recall is never below the exact ranking's and its usual cost is the walk's. The same size
225+ * turns the keyword leg from a read of the set's every chunk into a ranking decided on the row.
226+ */
227+ export const PERMITTED_EXACT_DOCUMENT_LIMIT = 5_000
228+
219229/** How long to stop trying the iterative-scan settings after the server rejected them. */
220230const HNSW_SETTINGS_UNSUPPORTED_RETRY_MS = 10 * 60 * 1000
221231
@@ -1766,6 +1776,49 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
17661776 )
17671777 }
17681778 let selected : SearchReadCandidate [ ]
1779+ /**
1780+ * The bounded ANN traversal is the whole candidate set. LIMIT keeps document
1781+ * authorization downstream of the traversal, with a primary-key lookup per candidate.
1782+ */
1783+ const scopeOfWalk = and (
1784+ inArray ( embeddingSearch . knowledgeBaseId , params . knowledgeBaseIds ) ,
1785+ eq ( embeddingSearch . enabled , true ) ,
1786+ excludedOnRow
1787+ )
1788+ const walkGraph = ( ) =>
1789+ withVectorScanSettings (
1790+ ( executor ) =>
1791+ executor . execute < SearchReadCandidate > (
1792+ plan
1793+ ? sql `
1794+ SELECT ${ PROJECTION_CANDIDATE_COLUMNS }
1795+ FROM ${ embeddingSearch } /* on-row visibility */
1796+ WHERE ${ and (
1797+ scopeOfWalk ,
1798+ projectionCandidateAccessCondition ( embeddingSearch , params . access , plan , { filled } ) ,
1799+ documentCondition === undefined
1800+ ? undefined
1801+ : sql `EXISTS (SELECT 1 FROM ${ document } WHERE ${ and ( eq ( document . id , embeddingSearch . documentId ) , documentCondition ) } )`
1802+ ) }
1803+ ORDER BY ${ candidateDistance } LIMIT ${ candidateLimit }
1804+ `
1805+ : sql `
1806+ SELECT ${ embeddingSearch . id } AS id, ${ embeddingSearch . documentId } AS "documentId",
1807+ visible.connector_id AS "connectorId"
1808+ FROM ${ embeddingSearch }
1809+ CROSS JOIN LATERAL (
1810+ SELECT ${ document . connectorId } AS connector_id FROM ${ document }
1811+ WHERE ${ and ( eq ( document . id , embeddingSearch . documentId ) , ...candidateDocumentVisibility , candidateTagCondition ) }
1812+ LIMIT 1
1813+ ) AS visible
1814+ WHERE ${ scopeOfWalk }
1815+ ORDER BY ${ candidateDistance } LIMIT ${ candidateLimit }
1816+ `
1817+ ) ,
1818+ params . budget ,
1819+ 'vector.candidate_search' ,
1820+ plan ? onRowWalkScanTuples ( documentCondition , filled ) : undefined
1821+ )
17691822 /**
17701823 * A source the caller is a member of that has its own index is walked on its own, which
17711824 * beats ranking it exactly once it is large enough to have earned that index.
@@ -1774,6 +1827,25 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
17741827 ( id ) => plannedIndexedSources ?. has ( id ) ?? false
17751828 )
17761829 if (
1830+ params . permitted ?. kind === 'bounded' &&
1831+ plan &&
1832+ filled &&
1833+ params . permitted . documents . length >= PERMITTED_EXACT_DOCUMENT_LIMIT
1834+ ) {
1835+ /**
1836+ * A set this large costs more to rank exactly than to walk: exact ranking reads every
1837+ * chunk of every document in it, while the walk decides readability on the rows it
1838+ * visits and stops at its tuple cap. The walk answers whenever the set is a fair share
1839+ * of the graph; where it is not, the walk underfills and the exact ranking that was
1840+ * always complete takes over, so nothing is lost but the walk's bounded cost.
1841+ */
1842+ selected = await walkGraph ( )
1843+ if ( selected . length < candidateLimit ) {
1844+ selected = await rankPermittedExactly (
1845+ params . permitted . documents . map ( ( entry ) => entry . id )
1846+ )
1847+ }
1848+ } else if (
17771849 params . permitted ?. kind === 'bounded' &&
17781850 ( ! walksASource || dateFilterCondition ( params . filters ) || params . filters ?. source )
17791851 ) {
@@ -1806,48 +1878,7 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
18061878 budget : params . budget ,
18071879 } )
18081880 } else {
1809- /**
1810- * The bounded ANN traversal is the whole candidate set. LIMIT keeps document
1811- * authorization downstream of the traversal, with a primary-key lookup per candidate.
1812- */
1813- const scopeOfWalk = and (
1814- inArray ( embeddingSearch . knowledgeBaseId , params . knowledgeBaseIds ) ,
1815- eq ( embeddingSearch . enabled , true ) ,
1816- excludedOnRow
1817- )
1818- selected = await withVectorScanSettings (
1819- ( executor ) =>
1820- executor . execute < SearchReadCandidate > (
1821- plan
1822- ? sql `
1823- SELECT ${ PROJECTION_CANDIDATE_COLUMNS }
1824- FROM ${ embeddingSearch } /* on-row visibility */
1825- WHERE ${ and (
1826- scopeOfWalk ,
1827- projectionCandidateAccessCondition ( embeddingSearch , params . access , plan , { filled } ) ,
1828- documentCondition === undefined
1829- ? undefined
1830- : sql `EXISTS (SELECT 1 FROM ${ document } WHERE ${ and ( eq ( document . id , embeddingSearch . documentId ) , documentCondition ) } )`
1831- ) }
1832- ORDER BY ${ candidateDistance } LIMIT ${ candidateLimit }
1833- `
1834- : sql `
1835- SELECT ${ embeddingSearch . id } AS id, ${ embeddingSearch . documentId } AS "documentId",
1836- visible.connector_id AS "connectorId"
1837- FROM ${ embeddingSearch }
1838- CROSS JOIN LATERAL (
1839- SELECT ${ document . connectorId } AS connector_id FROM ${ document }
1840- WHERE ${ and ( eq ( document . id , embeddingSearch . documentId ) , ...candidateDocumentVisibility , candidateTagCondition ) }
1841- LIMIT 1
1842- ) AS visible
1843- WHERE ${ scopeOfWalk }
1844- ORDER BY ${ candidateDistance } LIMIT ${ candidateLimit }
1845- `
1846- ) ,
1847- params . budget ,
1848- 'vector.candidate_search' ,
1849- plan ? onRowWalkScanTuples ( documentCondition , filled ) : undefined
1850- )
1881+ selected = await walkGraph ( )
18511882 /**
18521883 * A full traversal is already the nearest permitted chunks, so nothing else is worth
18531884 * running. An underfilled one is the signal that visibility removed neighbours the graph
@@ -2021,10 +2052,18 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise
20212052 * A caller reaching past the permitted-set limit reads much of the index, so ranking every
20222053 * match before checking access is the leg's whole cost for a common term. Where the Tin
20232054 * projection is complete, BM25 ranks inside the bases first and access is checked only on the
2024- * top of that ranking.
2055+ * top of that ranking. A bounded set past the exact-ranking size is read on the row like an
2056+ * unbounded one: the bounded read materializes every chunk of the set before it matches a
2057+ * term, where a ranking decided on the row costs what the term matches.
20252058 */
2059+ const accessPlan = access . kind === 'user' ? params . accessPlan : undefined
2060+ const largePermittedSet =
2061+ accessPlan !== undefined &&
2062+ params . permitted ?. kind === 'bounded' &&
2063+ params . permitted . documents . length >= PERMITTED_EXACT_DOCUMENT_LIMIT
2064+ const onRowReader = params . permitted ?. kind === 'unbounded' || largePermittedSet
20262065 let tinQuery : Awaited < ReturnType < typeof resolveTinKeywordQuery > > = null
2027- if ( params . permitted ?. kind === 'unbounded' && tagFilterConditions . length === 0 ) {
2066+ if ( onRowReader && tagFilterConditions . length === 0 ) {
20282067 try {
20292068 tinQuery = await resolveTinKeywordQuery (
20302069 params . searchIndexOnly === true ,
@@ -2038,9 +2077,7 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise
20382077 return [ ]
20392078 }
20402079 }
2041- if ( params . permitted ?. kind === 'unbounded' )
2042- annotateSearchDiagnostics ( { keywordRanking : tinQuery ? 'tin' : 'gin' } )
2043- const accessPlan = access . kind === 'user' ? params . accessPlan : undefined
2080+ if ( onRowReader ) annotateSearchDiagnostics ( { keywordRanking : tinQuery ? 'tin' : 'gin' } )
20442081 /** A filled projection decides readability on the ranked row alone; none of its rows needs the document. */
20452082 const tinFilled =
20462083 accessPlan && tinQuery
@@ -2098,8 +2135,7 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise
20982135 */
20992136 const narrow =
21002137 accessPlan !== undefined &&
2101- params . permitted ?. kind === 'unbounded' &&
2102- ! params . permitted . broad
2138+ ( ( params . permitted ?. kind === 'unbounded' && ! params . permitted . broad ) || largePermittedSet )
21032139 const windows : readonly number [ ] = narrow ? NARROW_KEYWORD_WINDOWS : TIN_KEYWORD_WINDOWS
21042140 /**
21052141 * A narrow reader's page is the readable remainder of a wide ranking, and that ranking is
@@ -2190,7 +2226,7 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise
21902226 * still re-applies the candidate predicate, so the restriction can only narrow.
21912227 */
21922228 const permittedIds =
2193- params . permitted ?. kind === 'bounded'
2229+ params . permitted ?. kind === 'bounded' && ! largePermittedSet
21942230 ? params . permitted . documents . map ( ( entry ) => entry . id )
21952231 : undefined
21962232 if ( permittedIds ?. length === 0 ) return { candidates : [ ] , nextOffset : offset }
0 commit comments