Skip to content

Commit 9191e3f

Browse files
committed
improvement(search): share the half-terms threshold and convert Jira descriptions only when filtered
1 parent e1a4ca2 commit 9191e3f

3 files changed

Lines changed: 44 additions & 36 deletions

File tree

‎apps/sim/lib/sim-search/live/atlassian.test.ts‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -344,24 +344,19 @@ describe('Jira plain search ranking', () => {
344344
titled: Issue[] | (() => never),
345345
half: Issue[] | (() => never) = []
346346
) {
347-
const issues = (rows: Issue[] | (() => never)) =>
348-
(typeof rows === 'function' ? rows() : rows).map((row) =>
349-
typeof row === 'string'
350-
? { key: row, fields: { summary: row } }
351-
: { key: row.key, fields: { summary: row.summary } }
352-
)
347+
const toRow = (row: Issue) => (typeof row === 'string' ? { key: row, summary: row } : row)
348+
const issues = (rows: Issue[]) =>
349+
rows.map(toRow).map(({ key, summary }) => ({ key, fields: { summary } }))
353350
const json = vi.fn(
354351
async (path: string, options?: { body?: { jql?: string; maxResults?: number } }) => {
355352
if (path === '/oauth/token/accessible-resources') return [SITE]
356353
if (path !== search) throw new Error(`Unexpected request: ${path}`)
357354
const jql = options?.body?.jql ?? ''
358-
if (jql.includes('summary ~'))
359-
return {
360-
issues: issues(jql.includes(' OR ') ? half : titled).slice(
361-
0,
362-
options?.body?.maxResults
363-
),
364-
}
355+
if (jql.includes('summary ~')) {
356+
const rows = jql.includes(' OR ') ? half : titled
357+
const picked = typeof rows === 'function' ? rows() : rows
358+
return { issues: issues(picked).slice(0, options?.body?.maxResults) }
359+
}
365360
return { issues: issues(exact.ids), ...(exact.more ? { nextPageToken: 'page-2' } : {}) }
366361
}
367362
)

‎apps/sim/lib/sim-search/live/atlassian.ts‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ async function sites(client: NativeClient) {
118118
}
119119

120120
/**
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.
121+
* Pair matches fetched for the half-the-terms summary fallback. Jira returns them newest first,
122+
* not by relevance, and the page is then filtered to summaries holding enough terms, so it starts
123+
* from a wider pool than the caller's limit or the any-term fallback's.
123124
*/
124125
const HALF_TERM_CANDIDATES = 50
125126

@@ -182,8 +183,11 @@ export async function searchAtlassian(
182183
const cloudId = string(site.id)
183184
const origin = string(site.url).replace(/\/$/, '')
184185
if (provider === 'jira') {
185-
/** One JQL page, each issue with its summary and description. */
186-
const searchIssues = async (jql: string, cursor?: string, maxResults = input.limit) => {
186+
/** One JQL page, each issue with its summary and raw fields. */
187+
const searchIssues = async (
188+
jql: string,
189+
{ cursor, maxResults = input.limit }: { cursor?: string; maxResults?: number } = {}
190+
) => {
187191
const data = object(
188192
await client.json(`/ex/jira/${segment(cloudId)}/rest/api/3/search/jql`, {
189193
body: {
@@ -201,20 +205,21 @@ export async function searchAtlassian(
201205
return {
202206
document: issue(row, cloudId, origin),
203207
summary: string(fields.summary),
204-
description: textContent(fields.description),
208+
fields,
205209
}
206210
}),
207211
next: string(data.nextPageToken) || undefined,
208212
}
209213
}
210214
const exact = await searchIssues(
211215
input.native?.query || (text ? jiraTextClause(text, input.textMatch) : ''),
212-
input.native?.cursor && single ? input.native.cursor : undefined
216+
{ cursor: input.native?.cursor && single ? input.native.cursor : undefined }
213217
)
214218
const exactPage: SitePage = {
215219
documents: exact.issues
216220
.filter(
217-
({ summary, description }) => !relevant || relevant(`${summary}\n${description}`)
221+
({ summary, fields }) =>
222+
!relevant || relevant(`${summary}\n${textContent(fields.description)}`)
218223
)
219224
.map(({ document }) => document),
220225
excluded: false,
@@ -229,22 +234,22 @@ export async function searchAtlassian(
229234
const [every, half] = await Promise.allSettled([
230235
searchIssues(jiraTextClause(text, 'all', 'summary')),
231236
terms.count >= 3
232-
? searchIssues(jiraTextClause(text, 'any', 'summary'), undefined, HALF_TERM_CANDIDATES)
237+
? searchIssues(jiraTextClause(text, 'any', 'summary'), {
238+
maxResults: HALF_TERM_CANDIDATES,
239+
})
233240
: undefined,
234241
])
235242
// A cancelled search stops here; any other failure keeps the all-terms page.
236243
input.signal?.throwIfAborted()
237-
const required = Math.ceil(terms.count / 2)
238-
const titled =
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)
247-
: []
244+
let titled =
245+
every.status === 'fulfilled' ? every.value.issues.map(({ document }) => document) : []
246+
if (!titled.length && half.status === 'fulfilled' && half.value) {
247+
titled = half.value.issues
248+
.map(({ document, summary }) => ({ document, held: terms.held(summary) }))
249+
.filter(({ held }) => held >= terms.required)
250+
.sort((left, right) => right.held - left.held)
251+
.map(({ document }) => document)
252+
}
248253
if (!titled.length) return exactPage
249254
const leading = new Set(titled.map((document) => document.id))
250255
return {

‎apps/sim/lib/sim-search/live/text-match.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,17 @@ export function termCoverage(text: string, terms: readonly string[]): number {
130130
* Query and text words are compared by their singular key, so a word and its regular plural count
131131
* as one term; a term with punctuation inside it is matched as written.
132132
*/
133-
export function stemmedTerms(query: string): { count: number; held: (text: string) => number } {
133+
export function stemmedTerms(query: string): {
134+
count: number
135+
/** At least half the terms, the minimum a partial match must hold. */
136+
required: number
137+
held: (text: string) => number
138+
} {
134139
const terms = distinctTerms(query, singularKey)
135140
const keys = terms.map(singularKey)
136141
return {
137142
count: terms.length,
143+
required: Math.ceil(terms.length / 2),
138144
held: (text) => {
139145
const words = new Set(textWords(text).map(singularKey))
140146
return terms.filter(
@@ -144,9 +150,11 @@ export function stemmedTerms(query: string): { count: number; held: (text: strin
144150
}
145151
}
146152

147-
/** A test for whether a text holds at least half the query's distinct terms, as `stemmedTerms` counts them. */
153+
/**
154+
* A test for whether a text holds at least half the query's distinct terms, as `stemmedTerms`
155+
* counts them.
156+
*/
148157
export function holdsHalfTheTerms(query: string): (text: string) => boolean {
149-
const { count, held } = stemmedTerms(query)
150-
const required = Math.ceil(count / 2)
158+
const { required, held } = stemmedTerms(query)
151159
return (text) => held(text) >= required
152160
}

0 commit comments

Comments
 (0)