@@ -13,7 +13,7 @@ import {
1313 confluenceTextClause ,
1414 holdsHalfTheTerms ,
1515 jiraTextClause ,
16- significantTerms ,
16+ stemmedTerms ,
1717} from '@/lib/sim-search/live/text-match'
1818import type {
1919 NativeClient ,
@@ -117,6 +117,12 @@ async function sites(client: NativeClient) {
117117 return array ( await client . json ( '/oauth/token/accessible-resources' , { memo : true } ) )
118118}
119119
120+ /**
121+ * Pair matches fetched for the half-the-terms summary fallback; the page is filtered to summaries
122+ * holding enough terms, so it starts from a wider pool than the caller's limit.
123+ */
124+ const HALF_TERM_CANDIDATES = 50
125+
120126/**
121127 * One Atlassian site's page; `ranking` marks a page whose order or completeness differs from the
122128 * all-terms search.
@@ -176,67 +182,75 @@ export async function searchAtlassian(
176182 const cloudId = string ( site . id )
177183 const origin = string ( site . url ) . replace ( / \/ $ / , '' )
178184 if ( provider === 'jira' ) {
179- /** One JQL page, keeping only issues whose summary and description pass `keep`. */
180- const searchIssues = async (
181- jql : string ,
182- cursor ?: string ,
183- keep ?: ( summary : string , description : string ) => boolean
184- ) : Promise < SitePage > => {
185+ /** One JQL page, each issue with its summary and description. */
186+ const searchIssues = async ( jql : string , cursor ?: string , maxResults = input . limit ) => {
185187 const data = object (
186188 await client . json ( `/ex/jira/${ segment ( cloudId ) } /rest/api/3/search/jql` , {
187189 body : {
188190 jql : scopeAtlassianQuery ( jql , scope , order ) ,
189- maxResults : input . limit ,
191+ maxResults,
190192 fields : [ 'summary' , 'description' , 'updated' , 'creator' , 'status' ] ,
191193 ...( cursor ? { nextPageToken : cursor } : { } ) ,
192194 } ,
193195 signal : input . signal ,
194196 } )
195197 )
196198 return {
197- documents : array ( data . issues )
198- . filter ( ( row ) => {
199- const fields = object ( row . fields )
200- return ! keep || keep ( string ( fields . summary ) , textContent ( fields . description ) )
201- } )
202- . map ( ( row ) => issue ( row , cloudId , origin ) ) ,
203- excluded : false ,
199+ issues : array ( data . issues ) . map ( ( row ) => {
200+ const fields = object ( row . fields )
201+ return {
202+ document : issue ( row , cloudId , origin ) ,
203+ summary : string ( fields . summary ) ,
204+ description : textContent ( fields . description ) ,
205+ }
206+ } ) ,
204207 next : string ( data . nextPageToken ) || undefined ,
205208 }
206209 }
207210 const exact = await searchIssues (
208211 input . native ?. query || ( text ? jiraTextClause ( text , input . textMatch ) : '' ) ,
209- input . native ?. cursor && single ? input . native . cursor : undefined ,
210- relevant && ( ( summary , description ) => relevant ( `${ summary } \n${ description } ` ) )
212+ input . native ?. cursor && single ? input . native . cursor : undefined
211213 )
214+ const exactPage : SitePage = {
215+ documents : exact . issues
216+ . filter (
217+ ( { summary, description } ) => ! relevant || relevant ( `${ summary } \n${ description } ` )
218+ )
219+ . map ( ( { document } ) => document ) ,
220+ excluded : false ,
221+ next : exact . next ,
222+ }
212223 // JQL has no relevance order, so an all-terms match larger than a page comes back newest
213224 // first; issues whose summary holds every term, or else at least half of three or more
214- // terms, lead it instead.
215- if ( ! text || input . native || order || input . textMatch === 'any' || ! exact . next ) return exact
225+ // terms (most terms first), lead it instead.
226+ if ( ! text || input . native || order || input . textMatch === 'any' || ! exact . next )
227+ return exactPage
228+ const terms = stemmedTerms ( text )
216229 const [ every , half ] = await Promise . allSettled ( [
217230 searchIssues ( jiraTextClause ( text , 'all' , 'summary' ) ) ,
218- significantTerms ( text ) . length >= 3
219- ? searchIssues (
220- jiraTextClause ( text , 'any' , 'summary' ) ,
221- undefined ,
222- holdsHalfTheTerms ( text )
223- )
231+ terms . count >= 3
232+ ? searchIssues ( jiraTextClause ( text , 'any' , 'summary' ) , undefined , HALF_TERM_CANDIDATES )
224233 : undefined ,
225234 ] )
226235 // A cancelled search stops here; any other failure keeps the all-terms page.
227236 input . signal ?. throwIfAborted ( )
237+ const required = Math . ceil ( terms . count / 2 )
228238 const titled =
229- every . status === 'fulfilled' && every . value . documents . length
230- ? every . value . documents
231- : half . status === 'fulfilled'
232- ? ( half . value ?. documents ?? [ ] )
239+ every . status === 'fulfilled' && every . value . issues . length
240+ ? every . value . issues . map ( ( { document } ) => document )
241+ : half . status === 'fulfilled' && half . value
242+ ? half . value . issues
243+ . map ( ( { document, summary } ) => ( { document, held : terms . held ( summary ) } ) )
244+ . filter ( ( { held } ) => held >= required )
245+ . sort ( ( left , right ) => right . held - left . held )
246+ . map ( ( { document } ) => document )
233247 : [ ]
234- if ( ! titled . length ) return exact
248+ if ( ! titled . length ) return exactPage
235249 const leading = new Set ( titled . map ( ( document ) => document . id ) )
236250 return {
237251 documents : [
238252 ...titled ,
239- ...exact . documents . filter ( ( document ) => ! leading . has ( document . id ) ) ,
253+ ...exactPage . documents . filter ( ( document ) => ! leading . has ( document . id ) ) ,
240254 ] . slice ( 0 , input . limit ) ,
241255 excluded : false ,
242256 ranking : 'relevance' ,
@@ -304,7 +318,7 @@ export async function searchAtlassian(
304318 : '' ) +
305319 ( pages . some ( ( result ) => result . ranking === 'relevance' )
306320 ? provider === 'jira'
307- ? ' More issues contained every word than fit on one page, so issues whose summary holds the most query words come first; refine the query, or page with a native JQL query.'
321+ ? ' More issues contained every word than fit on one page, so issues whose summary holds every word, or else most of them, come first; refine the query, or page with a native JQL query.'
308322 : ' More pages contained every word than fit on one page, so this page is ordered by Confluence relevance search; refine the query, or page with a native CQL query.'
309323 : '' ) +
310324 ( pages . some ( ( result ) => result . ranking === 'unavailable' )
0 commit comments