From fa94639d53564bf0d0b092a67c1a146af21bff3d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 16 Oct 2026 02:01:51 -0700 Subject: [PATCH 1/2] fix(search): keep the Jira half-terms summary tier within the all-terms match --- .../sim/lib/sim-search/live/atlassian.test.ts | 20 +++++++++++++++++-- apps/sim/lib/sim-search/live/atlassian.ts | 19 ++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/sim-search/live/atlassian.test.ts b/apps/sim/lib/sim-search/live/atlassian.test.ts index 13d36f8d857..960af418cf7 100644 --- a/apps/sim/lib/sim-search/live/atlassian.test.ts +++ b/apps/sim/lib/sim-search/live/atlassian.test.ts @@ -334,7 +334,8 @@ describe('Confluence plain search ranking', () => { describe('Jira plain search ranking', () => { const search = '/ex/jira/cloud/rest/api/3/search/jql' - type Issue = string | { key: string; summary: string } + /** `outsideMatch` marks an issue whose text lacks a query term, so an all-terms clause excludes it. */ + type Issue = string | { key: string; summary: string; outsideMatch?: boolean } /** * Answers the all-terms query with `exact`, the all-terms summary query with `titled`, and the * half-the-terms summary query with `half`, or their failures. @@ -354,7 +355,9 @@ describe('Jira plain search ranking', () => { const jql = options?.body?.jql ?? '' if (jql.includes('summary ~')) { const rows = jql.includes(' OR ') ? half : titled - const picked = typeof rows === 'function' ? rows() : rows + const picked = (typeof rows === 'function' ? rows() : rows).filter( + (row) => !jql.includes('text ~') || typeof row === 'string' || !row.outsideMatch + ) return { issues: issues(picked).slice(0, options?.body?.maxResults) } } return { issues: issues(exact.ids), ...(exact.more ? { nextPageToken: 'page-2' } : {}) } @@ -502,4 +505,17 @@ describe('Jira plain search ranking', () => { const page = await searchAtlassian(api, 'jira', query) expect(page.documents.map((document) => document.id)).toEqual(['ALL-1', 'HALF-1', 'NEW-3']) }) + + it('keeps half-the-terms summaries to issues that match every term somewhere', async () => { + const api = jira( + overflowing, + [], + [ + { key: 'OUTSIDE-1', summary: 'Merged results dashboard', outsideMatch: true }, + { key: 'INSIDE-1', summary: 'Merged results broke the dashboard' }, + ] + ) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['INSIDE-1', 'NEW-3', 'NEW-2']) + }) }) diff --git a/apps/sim/lib/sim-search/live/atlassian.ts b/apps/sim/lib/sim-search/live/atlassian.ts index b355b316c83..bcafe841591 100644 --- a/apps/sim/lib/sim-search/live/atlassian.ts +++ b/apps/sim/lib/sim-search/live/atlassian.ts @@ -118,9 +118,10 @@ async function sites(client: NativeClient) { } /** - * Pair matches fetched for the half-the-terms summary fallback. Jira returns them newest first, - * not by relevance, and the page is then filtered to summaries holding enough terms, so it starts - * from a wider pool than the caller's limit or the any-term fallback's. + * Issues fetched for the half-the-terms summary tier: summary pair matches within the all-terms + * match. Jira returns them newest first, not by relevance, and the page is then filtered to + * summaries holding enough terms, so it starts from a wider pool than the caller's limit or the + * any-term fallback's. */ const HALF_TERM_CANDIDATES = 50 @@ -226,17 +227,19 @@ export async function searchAtlassian( next: exact.next, } // JQL has no relevance order, so an all-terms match larger than a page comes back newest - // first; issues whose summary holds every term lead it instead, then those holding at - // least half of three or more terms (most terms first). + // first. Within that match, issues whose summary holds every term lead it instead, then + // those whose summary holds at least half of three or more terms (most terms first); a + // summary match never brings in an issue lacking one of the terms. if (!text || input.native || order || input.textMatch === 'any' || !exact.next) return exactPage const terms = stemmedTerms(text) const [every, half] = await Promise.allSettled([ searchIssues(jiraTextClause(text, 'all', 'summary')), terms.count >= 3 - ? searchIssues(jiraTextClause(text, 'any', 'summary'), { - maxResults: HALF_TERM_CANDIDATES, - }) + ? searchIssues( + `${jiraTextClause(text, 'any', 'summary')} AND ${jiraTextClause(text)}`, + { maxResults: HALF_TERM_CANDIDATES } + ) : undefined, ]) // A cancelled search stops here; any other failure keeps the all-terms page. From 6d61d2f119bf24c1d21bdc8798d6a3dc16dea433 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 16 Oct 2026 02:04:05 -0700 Subject: [PATCH 2/2] test(search): normalize Jira fake rows before filtering --- apps/sim/lib/sim-search/live/atlassian.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/sim/lib/sim-search/live/atlassian.test.ts b/apps/sim/lib/sim-search/live/atlassian.test.ts index 960af418cf7..c9d608f6203 100644 --- a/apps/sim/lib/sim-search/live/atlassian.test.ts +++ b/apps/sim/lib/sim-search/live/atlassian.test.ts @@ -334,7 +334,7 @@ describe('Confluence plain search ranking', () => { describe('Jira plain search ranking', () => { const search = '/ex/jira/cloud/rest/api/3/search/jql' - /** `outsideMatch` marks an issue whose text lacks a query term, so an all-terms clause excludes it. */ + /** `outsideMatch`: the issue lacks a query term, so a `text ~` all-terms clause drops it. */ type Issue = string | { key: string; summary: string; outsideMatch?: boolean } /** * Answers the all-terms query with `exact`, the all-terms summary query with `titled`, and the @@ -345,7 +345,8 @@ describe('Jira plain search ranking', () => { titled: Issue[] | (() => never), half: Issue[] | (() => never) = [] ) { - const toRow = (row: Issue) => (typeof row === 'string' ? { key: row, summary: row } : row) + const toRow = (row: Issue): Exclude => + typeof row === 'string' ? { key: row, summary: row } : row const issues = (rows: Issue[]) => rows.map(toRow).map(({ key, summary }) => ({ key, fields: { summary } })) const json = vi.fn( @@ -355,9 +356,9 @@ describe('Jira plain search ranking', () => { const jql = options?.body?.jql ?? '' if (jql.includes('summary ~')) { const rows = jql.includes(' OR ') ? half : titled - const picked = (typeof rows === 'function' ? rows() : rows).filter( - (row) => !jql.includes('text ~') || typeof row === 'string' || !row.outsideMatch - ) + const picked = (typeof rows === 'function' ? rows() : rows) + .map(toRow) + .filter((row) => !(row.outsideMatch && jql.includes('text ~'))) return { issues: issues(picked).slice(0, options?.body?.maxResults) } } return { issues: issues(exact.ids), ...(exact.more ? { nextPageToken: 'page-2' } : {}) }