Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions apps/sim/lib/sim-search/live/atlassian.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`: 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
* half-the-terms summary query with `half`, or their failures.
Expand All @@ -344,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<Issue, string> =>
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(
Expand All @@ -354,7 +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
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' } : {}) }
Expand Down Expand Up @@ -502,4 +506,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'])
})
})
19 changes: 11 additions & 8 deletions apps/sim/lib/sim-search/live/atlassian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading