From 84691c55863f4d2f9f88dbffed1972609b81e407 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 13:50:56 -0700 Subject: [PATCH 1/6] improvement(search): keep a Jira partial match only when it holds half the query's terms The any-term retry ORs pairs of terms, so on a large Jira site it returns tickets sharing only a couple of common words, which then ranked alongside every other source's best results. Jira partial matches must now contain at least half the query's distinct terms in their title or description, a word and its regular plural counting as one as Jira's stemmed search matches them (minimum-should-match over the relaxed tier). Confluence's relevance-ranked site search is unchanged. When no partial match qualifies, the all-terms page is returned as it was before the retry existed. --- .../lib/sim-search/live/account-session.ts | 5 +- .../lib/sim-search/live/application.test.ts | 1 + apps/sim/lib/sim-search/live/providers.ts | 14 +++++ .../lib/sim-search/live/term-fallback.test.ts | 57 +++++++++++++++++++ apps/sim/lib/sim-search/live/term-fallback.ts | 19 ++++++- apps/sim/lib/sim-search/live/text-match.ts | 16 ++++++ 6 files changed, 108 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/sim-search/live/account-session.ts b/apps/sim/lib/sim-search/live/account-session.ts index c343e894fd1..f52d2bc1615 100644 --- a/apps/sim/lib/sim-search/live/account-session.ts +++ b/apps/sim/lib/sim-search/live/account-session.ts @@ -23,6 +23,7 @@ import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/pol import { LIVE_SEARCH_PROVIDER_CATALOG } from '@/lib/sim-search/live/provider-catalog' import { isSearchProviderEnabled } from '@/lib/sim-search/live/provider-rollout' import { + partialMatchesNeedHalfTheTerms, previewNativeText, readNativeProvider, requiresEveryTerm, @@ -206,7 +207,9 @@ export async function openLiveAccountSession( ) } return client && requiresEveryTerm(provider) - ? searchWithAnyTermFallback(searchOnce, search, signal) + ? searchWithAnyTermFallback(searchOnce, search, signal, { + requireHalfTheTerms: partialMatchesNeedHalfTheTerms(provider), + }) : searchOnce(search) }, async verify(document) { diff --git a/apps/sim/lib/sim-search/live/application.test.ts b/apps/sim/lib/sim-search/live/application.test.ts index 7591c26a748..8711012a52b 100644 --- a/apps/sim/lib/sim-search/live/application.test.ts +++ b/apps/sim/lib/sim-search/live/application.test.ts @@ -68,6 +68,7 @@ vi.mock('@/lib/sim-search/live/providers', () => ({ searchNativeProvider: mocks.search, readNativeProvider: mocks.read, requiresEveryTerm: mocks.requiresEveryTerm, + partialMatchesNeedHalfTheTerms: () => false, previewNativeText: mocks.preview, })) vi.mock('@/executor/utils/resolved-secret-content-projection', () => ({ diff --git a/apps/sim/lib/sim-search/live/providers.ts b/apps/sim/lib/sim-search/live/providers.ts index 7333304ae55..93b334c53ec 100644 --- a/apps/sim/lib/sim-search/live/providers.ts +++ b/apps/sim/lib/sim-search/live/providers.ts @@ -48,6 +48,11 @@ interface NativeProvider { guide: NativeQueryGuide /** Plain queries must contain every word, so an underfilled search retries with any term. */ requiresEveryTerm?: true + /** + * The any-term retry ranks items sharing just a couple of common words alongside close matches, + * so its partial matches must contain at least half the query's terms. + */ + partialMatchesNeedHalfTheTerms?: true /** * Full text for a search preview, for providers whose search returns only an excerpt or * metadata. At most one request; undefined for kinds without a cheap text form. @@ -177,6 +182,7 @@ export const LIVE_SEARCH_PROVIDERS = { 'JQL with only an ORDER BY clause (Jira rejects unbounded queries), and identifying users other than currentUser() by display name or email; use their account ID.', }, requiresEveryTerm: true, + partialMatchesNeedHalfTheTerms: true, search: (client, input) => searchAtlassian(client, 'jira', input), read: (client, reference) => readAtlassian(client, 'jira', reference.id, reference.container), }, @@ -319,6 +325,14 @@ export function requiresEveryTerm(provider: LiveSearchProviderId): boolean { return 'requiresEveryTerm' in adapter && adapter.requiresEveryTerm === true } +/** True when the provider's partial matches must contain at least half the query's terms. */ +export function partialMatchesNeedHalfTheTerms(provider: LiveSearchProviderId): boolean { + const adapter = LIVE_SEARCH_PROVIDERS[provider] + return ( + 'partialMatchesNeedHalfTheTerms' in adapter && adapter.partialMatchesNeedHalfTheTerms === true + ) +} + /** * Full text for a search result's preview, for providers whose search returns only an excerpt or * metadata. Undefined when the provider or the document's kind has no single-request text form. diff --git a/apps/sim/lib/sim-search/live/term-fallback.test.ts b/apps/sim/lib/sim-search/live/term-fallback.test.ts index 34c1536789f..bfb64298a4d 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.test.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.test.ts @@ -217,6 +217,63 @@ describe('searchWithAnyTermFallback', () => { expect(search).toHaveBeenCalledTimes(1) }) + describe('when partial matches must contain half the terms', () => { + const long: NativeSearchInput = { + query: 'glean token creator role request', + limit: 10, + scopes: [], + } + const halfTheTerms = { requireHalfTheTerms: true } + + it('drops a partial match containing fewer than half the terms', async () => { + const search = provider( + page(), + page( + { id: 'two-terms', title: 'Role request form' }, + { id: 'three-terms', title: 'Token creator role' }, + { id: 'in-description', title: 'Access', content: 'Glean token request for my team' } + ) + ) + const result = await searchWithAnyTermFallback(search, long, live, halfTheTerms) + expect(result.documents.map((document) => document.id)).toEqual([ + 'three-terms', + 'in-description', + ]) + }) + + it('counts a word and its plural as one term, as the provider matches them', async () => { + const search = provider( + page(), + page({ id: 'plurals', title: 'Glean tokens', content: 'Creators file requests' }) + ) + const result = await searchWithAnyTermFallback(search, long, live, halfTheTerms) + expect(result.documents.map((document) => document.id)).toEqual(['plurals']) + }) + + it('returns the all-terms page unchanged when no partial match is close enough', async () => { + const strict = page({ id: 'exact', title: 'Glean token creator role request' }) + const search = provider(strict, page({ id: 'loose', title: 'Role request' })) + expect(await searchWithAnyTermFallback(search, long, live, halfTheTerms)).toBe(strict) + }) + + it('keeps a single matching term enough for a two-term query', async () => { + const search = provider(page(), page({ id: 'one', title: 'Contractor directory' })) + const result = await searchWithAnyTermFallback( + search, + { query: 'contractor offboarding', limit: 4, scopes: [] }, + live, + halfTheTerms + ) + expect(result.documents.map((document) => document.id)).toEqual(['one']) + }) + + it('leaves providers without the floor adding loose partial matches', async () => { + const search = provider(page(), page({ id: 'two-terms', title: 'Role request form' })) + const result = await searchWithAnyTermFallback(search, long, live) + expect(result.documents.map((document) => document.id)).toEqual(['two-terms']) + }) + }) + it('leaves the page untouched when the any-term search adds nothing new', async () => { const strict = page({ id: 'exact' }) const search = provider(strict, page({ id: 'exact' })) diff --git a/apps/sim/lib/sim-search/live/term-fallback.ts b/apps/sim/lib/sim-search/live/term-fallback.ts index b688e937a0d..63e5cb86de8 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.ts @@ -3,7 +3,11 @@ import { getErrorMessage } from '@sim/utils/errors' import { dateSortDirection } from '@/lib/sim-search/live/dates' import { NativeSearchError } from '@/lib/sim-search/live/http' import { joinMessages } from '@/lib/sim-search/live/pages' -import { significantTerms, termCoverage } from '@/lib/sim-search/live/text-match' +import { + containsHalfTheTerms, + significantTerms, + termCoverage, +} from '@/lib/sim-search/live/text-match' import type { NativeDocument, NativePage, NativeSearchInput } from '@/lib/sim-search/live/types' /** @@ -42,11 +46,15 @@ const documentKey = ({ container, kind, id }: NativeDocument) => * slow OR query is cancelled rather than awaited. Whenever it fails short of the caller's own * cancellation (a provider refusal, the budget running out, a transport error), the all-terms page * is kept, reported as partial. Cancellation through `signal` propagates like the first search. + * + * With `requireHalfTheTerms`, a partial match must contain at least half the query's terms in its + * title or text, for providers whose OR query matches items sharing only a couple of common words. */ export async function searchWithAnyTermFallback( search: (input: NativeSearchInput) => Promise, input: NativeSearchInput, - signal: AbortSignal + signal: AbortSignal, + options: { requireHalfTheTerms?: boolean } = {} ): Promise { const strict = await search(input) const terms = significantTerms(input.query) @@ -93,7 +101,12 @@ export async function searchWithAnyTermFallback( } const seen = new Set(strict.documents.map(documentKey)) const additions = relaxed.documents - .filter((document) => !seen.has(documentKey(document))) + .filter( + (document) => + !seen.has(documentKey(document)) && + (!options.requireHalfTheTerms || + containsHalfTheTerms(`${document.title}\n${document.content}`, input.query)) + ) .map((document, rank) => ({ document, rank, diff --git a/apps/sim/lib/sim-search/live/text-match.ts b/apps/sim/lib/sim-search/live/text-match.ts index 7160078d6d3..b3825c02bc4 100644 --- a/apps/sim/lib/sim-search/live/text-match.ts +++ b/apps/sim/lib/sim-search/live/text-match.ts @@ -115,3 +115,19 @@ export function confluenceTextClause(text: string, match: TextMatch = 'all'): st export function termCoverage(text: string, terms: readonly string[]): number { return terms.filter((term) => findTermMatches(text, [term]).length > 0).length } + +/** + * Whether the text holds at least half the query's distinct terms, a word and its regular plural + * counting as one term as a stemmed search matches them. + */ +export function containsHalfTheTerms(text: string, query: string): boolean { + const terms = distinctTerms(query, singularKey).map(singularKey) + const present = terms.filter((term) => findTermMatches(text, wordForms(term)).length > 0) + return present.length >= Math.ceil(terms.length / 2) +} + +/** A singular word and its regular plural forms, longest first so a plural is matched whole. */ +function wordForms(singular: string): string[] { + const forms = [`${singular}es`, `${singular}s`, singular] + return singular.endsWith('y') ? [`${singular.slice(0, -1)}ies`, ...forms] : forms +} From 55fa2737e913c53738d877de93827edc99b9c577 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 13:53:12 -0700 Subject: [PATCH 2/6] improvement(search): one provider setting for the any-term retry Replace the parallel requiresEveryTerm and partialMatchesNeedHalfTheTerms flags and their two lookups with a single anyTermRetry setting that carries the fallback's options. --- .../lib/sim-search/live/account-session.ts | 10 +++--- .../lib/sim-search/live/application.test.ts | 9 +++--- apps/sim/lib/sim-search/live/providers.ts | 32 ++++++++----------- apps/sim/lib/sim-search/live/term-fallback.ts | 7 +++- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/apps/sim/lib/sim-search/live/account-session.ts b/apps/sim/lib/sim-search/live/account-session.ts index f52d2bc1615..8b9f543e4ac 100644 --- a/apps/sim/lib/sim-search/live/account-session.ts +++ b/apps/sim/lib/sim-search/live/account-session.ts @@ -23,10 +23,9 @@ import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/pol import { LIVE_SEARCH_PROVIDER_CATALOG } from '@/lib/sim-search/live/provider-catalog' import { isSearchProviderEnabled } from '@/lib/sim-search/live/provider-rollout' import { - partialMatchesNeedHalfTheTerms, + anyTermRetry, previewNativeText, readNativeProvider, - requiresEveryTerm, searchNativeProvider, } from '@/lib/sim-search/live/providers' import { searchWithinPolicy } from '@/lib/sim-search/live/scoped-search' @@ -206,10 +205,9 @@ export async function openLiveAccountSession( client ? searchNativeProvider(provider, client, native) : searchMcp(native) ) } - return client && requiresEveryTerm(provider) - ? searchWithAnyTermFallback(searchOnce, search, signal, { - requireHalfTheTerms: partialMatchesNeedHalfTheTerms(provider), - }) + const retry = client ? anyTermRetry(provider) : undefined + return retry + ? searchWithAnyTermFallback(searchOnce, search, signal, retry) : searchOnce(search) }, async verify(document) { diff --git a/apps/sim/lib/sim-search/live/application.test.ts b/apps/sim/lib/sim-search/live/application.test.ts index 8711012a52b..0591419d86c 100644 --- a/apps/sim/lib/sim-search/live/application.test.ts +++ b/apps/sim/lib/sim-search/live/application.test.ts @@ -26,7 +26,7 @@ const mocks = vi.hoisted(() => ({ json: vi.fn(), service: vi.fn(), destroyPool: vi.fn(), - requiresEveryTerm: vi.fn(), + anyTermRetry: vi.fn(), preview: vi.fn(), })) vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) @@ -67,8 +67,7 @@ vi.mock('@/lib/sim-search/live/providers', () => ({ liveSearchGuidance: (providers: string[]) => `Live coverage: ${[...new Set(providers)]}`, searchNativeProvider: mocks.search, readNativeProvider: mocks.read, - requiresEveryTerm: mocks.requiresEveryTerm, - partialMatchesNeedHalfTheTerms: () => false, + anyTermRetry: mocks.anyTermRetry, previewNativeText: mocks.preview, })) vi.mock('@/executor/utils/resolved-secret-content-projection', () => ({ @@ -141,7 +140,7 @@ describe('authorized live retrieval', () => { documents: [{ ...document, id: 'src/a.ts', container: '42', kind: 'code' }], }) mocks.adminVerify.mockResolvedValue(true) - mocks.requiresEveryTerm.mockReset().mockReturnValue(false) + mocks.anyTermRetry.mockReset().mockReturnValue(undefined) mocks.preview.mockReset().mockResolvedValue(undefined) }) describe('Zoom continuation integrity', () => { @@ -699,7 +698,7 @@ describe('authorized live retrieval', () => { }) it('retries an underfilled search with any term for providers that require every term', async () => { - mocks.requiresEveryTerm.mockReturnValue(true) + mocks.anyTermRetry.mockReturnValue({}) const loose = { ...document, id: 'loose', url: 'https://docs.google.com/document/d/loose/edit' } mocks.search.mockImplementation(async (_provider, _client, search) => ({ documents: search.textMatch === 'any' ? [loose, document] : [document], diff --git a/apps/sim/lib/sim-search/live/providers.ts b/apps/sim/lib/sim-search/live/providers.ts index 93b334c53ec..6e987e3ad16 100644 --- a/apps/sim/lib/sim-search/live/providers.ts +++ b/apps/sim/lib/sim-search/live/providers.ts @@ -20,6 +20,7 @@ import { type LiveSearchProviderId, } from '@/lib/sim-search/live/provider-catalog' import { readSlack, searchSlack } from '@/lib/sim-search/live/slack' +import type { AnyTermFallbackOptions } from '@/lib/sim-search/live/term-fallback' import type { NativeClient, NativeDocument, @@ -46,13 +47,12 @@ interface NativeQueryGuide { interface NativeProvider { guide: NativeQueryGuide - /** Plain queries must contain every word, so an underfilled search retries with any term. */ - requiresEveryTerm?: true /** - * The any-term retry ranks items sharing just a couple of common words alongside close matches, - * so its partial matches must contain at least half the query's terms. + * Plain queries must contain every word, so an underfilled search retries with any term. + * `requireHalfTheTerms` is for a provider whose any-term query ranks items sharing just a couple + * of common words alongside close matches. */ - partialMatchesNeedHalfTheTerms?: true + anyTermRetry?: AnyTermFallbackOptions /** * Full text for a search preview, for providers whose search returns only an excerpt or * metadata. At most one request; undefined for kinds without a cheap text form. @@ -181,8 +181,7 @@ export const LIVE_SEARCH_PROVIDERS = { avoid: 'JQL with only an ORDER BY clause (Jira rejects unbounded queries), and identifying users other than currentUser() by display name or email; use their account ID.', }, - requiresEveryTerm: true, - partialMatchesNeedHalfTheTerms: true, + anyTermRetry: { requireHalfTheTerms: true }, search: (client, input) => searchAtlassian(client, 'jira', input), read: (client, reference) => readAtlassian(client, 'jira', reference.id, reference.container), }, @@ -196,7 +195,7 @@ export const LIVE_SEARCH_PROVIDERS = { avoid: 'starting the query with a negative clause (NOT, !=, !~ or NOT IN), which CQL rejects; lead with a positive clause such as type IN (page, blogpost).', }, - requiresEveryTerm: true, + anyTermRetry: { requireHalfTheTerms: false }, previewText: previewConfluence, search: (client, input) => searchAtlassian(client, 'confluence', input), read: (client, reference) => @@ -319,18 +318,13 @@ export function searchNativeProvider( return adapter.search(client, input) } -/** True when the provider's plain-text search matches only items containing every query word. */ -export function requiresEveryTerm(provider: LiveSearchProviderId): boolean { - const adapter = LIVE_SEARCH_PROVIDERS[provider] - return 'requiresEveryTerm' in adapter && adapter.requiresEveryTerm === true -} - -/** True when the provider's partial matches must contain at least half the query's terms. */ -export function partialMatchesNeedHalfTheTerms(provider: LiveSearchProviderId): boolean { +/** + * How the provider retries an underfilled plain search with any term; undefined when its + * plain-text search does not require every query word. + */ +export function anyTermRetry(provider: LiveSearchProviderId): AnyTermFallbackOptions | undefined { const adapter = LIVE_SEARCH_PROVIDERS[provider] - return ( - 'partialMatchesNeedHalfTheTerms' in adapter && adapter.partialMatchesNeedHalfTheTerms === true - ) + return 'anyTermRetry' in adapter ? adapter.anyTermRetry : undefined } /** diff --git a/apps/sim/lib/sim-search/live/term-fallback.ts b/apps/sim/lib/sim-search/live/term-fallback.ts index 63e5cb86de8..c21edf7a72c 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.ts @@ -50,11 +50,16 @@ const documentKey = ({ container, kind, id }: NativeDocument) => * With `requireHalfTheTerms`, a partial match must contain at least half the query's terms in its * title or text, for providers whose OR query matches items sharing only a couple of common words. */ +export interface AnyTermFallbackOptions { + /** Keep only partial matches holding at least half the query's terms. */ + requireHalfTheTerms?: boolean +} + export async function searchWithAnyTermFallback( search: (input: NativeSearchInput) => Promise, input: NativeSearchInput, signal: AbortSignal, - options: { requireHalfTheTerms?: boolean } = {} + options: AnyTermFallbackOptions = {} ): Promise { const strict = await search(input) const terms = significantTerms(input.query) From 1ad94ffaf736c61f5f0ce8f0af43e72203307feb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 13:57:36 -0700 Subject: [PATCH 3/6] fix(search): compare query and ticket words by the same singular key in the Jira floor Rebuilding plural forms from a word's key missed singular words whose plural adds -es (release, database, license): their key drops the final e, so neither the word nor its plural was found and the floor dropped valid partial matches. Both sides now go through the same key, as an analyzer does for query and document, with a term containing punctuation matched as written. --- .../lib/sim-search/live/term-fallback.test.ts | 28 +++++++++++++++++++ apps/sim/lib/sim-search/live/text-match.ts | 18 ++++++------ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/sim-search/live/term-fallback.test.ts b/apps/sim/lib/sim-search/live/term-fallback.test.ts index bfb64298a4d..4e9220634ad 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.test.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.test.ts @@ -250,6 +250,34 @@ describe('searchWithAnyTermFallback', () => { expect(result.documents.map((document) => document.id)).toEqual(['plurals']) }) + it('counts a singular word whose plural adds -es, in either form', async () => { + const search = provider( + page(), + page( + { id: 'singular', title: 'Database release' }, + { id: 'plural', title: 'Databases', content: 'Weekly releases' } + ) + ) + const result = await searchWithAnyTermFallback( + search, + { query: 'database release checklist owner', limit: 10, scopes: [] }, + live, + halfTheTerms + ) + expect(result.documents.map((document) => document.id).sort()).toEqual(['plural', 'singular']) + }) + + it('counts a term with punctuation inside it, such as ci/cd', async () => { + const search = provider(page(), page({ id: 'ci', title: 'CI/CD pipeline' })) + const result = await searchWithAnyTermFallback( + search, + { query: 'ci/cd pipeline failure owner', limit: 10, scopes: [] }, + live, + halfTheTerms + ) + expect(result.documents.map((document) => document.id)).toEqual(['ci']) + }) + it('returns the all-terms page unchanged when no partial match is close enough', async () => { const strict = page({ id: 'exact', title: 'Glean token creator role request' }) const search = provider(strict, page({ id: 'loose', title: 'Role request' })) diff --git a/apps/sim/lib/sim-search/live/text-match.ts b/apps/sim/lib/sim-search/live/text-match.ts index b3825c02bc4..494d5dfb103 100644 --- a/apps/sim/lib/sim-search/live/text-match.ts +++ b/apps/sim/lib/sim-search/live/text-match.ts @@ -117,17 +117,15 @@ export function termCoverage(text: string, terms: readonly string[]): number { } /** - * Whether the text holds at least half the query's distinct terms, a word and its regular plural - * counting as one term as a stemmed search matches them. + * Whether the text holds at least half the query's distinct terms. Query and text words are + * compared by their singular key, so a word and its regular plural count as one term as a stemmed + * search matches them; a term with punctuation inside it is matched as written. */ export function containsHalfTheTerms(text: string, query: string): boolean { - const terms = distinctTerms(query, singularKey).map(singularKey) - const present = terms.filter((term) => findTermMatches(text, wordForms(term)).length > 0) + const terms = distinctTerms(query, singularKey) + const words = new Set(text.match(/[\p{L}\p{N}_]+/gu)?.map(singularKey)) + const present = terms.filter( + (term) => words.has(singularKey(term)) || findTermMatches(text, [term]).length > 0 + ) return present.length >= Math.ceil(terms.length / 2) } - -/** A singular word and its regular plural forms, longest first so a plural is matched whole. */ -function wordForms(singular: string): string[] { - const forms = [`${singular}es`, `${singular}s`, singular] - return singular.endsWith('y') ? [`${singular.slice(0, -1)}ies`, ...forms] : forms -} From 98af94f3c9b5af6dea9cf51fbf31cac7269618b8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 14:01:43 -0700 Subject: [PATCH 4/6] refactor(search): apply the Jira half-the-terms floor in the Jira adapter The floor tightens Jira's own any-term query, so it now sits beside that query in the Jira branch of searchAtlassian. The shared any-term fallback and the provider registry are back to their previous shape: no option, flag, or exported type. The term test is built once per search from the query, splits words with the shared WORD_CHARACTER definition, and is covered directly in text-match tests, with the adapter tests covering the wiring and Confluence staying untouched. --- apps/sim/lib/knowledge/search/snippet.ts | 6 ++ .../lib/sim-search/live/account-session.ts | 7 +- .../lib/sim-search/live/application.test.ts | 8 +- .../sim/lib/sim-search/live/atlassian.test.ts | 33 +++++++ apps/sim/lib/sim-search/live/atlassian.ts | 15 +++- apps/sim/lib/sim-search/live/providers.ts | 22 ++--- .../lib/sim-search/live/term-fallback.test.ts | 85 ------------------- apps/sim/lib/sim-search/live/term-fallback.ts | 24 +----- .../lib/sim-search/live/text-match.test.ts | 31 ++++++- apps/sim/lib/sim-search/live/text-match.ts | 24 +++--- 10 files changed, 113 insertions(+), 142 deletions(-) diff --git a/apps/sim/lib/knowledge/search/snippet.ts b/apps/sim/lib/knowledge/search/snippet.ts index 2315eeb0f98..cb604e143ee 100644 --- a/apps/sim/lib/knowledge/search/snippet.ts +++ b/apps/sim/lib/knowledge/search/snippet.ts @@ -16,6 +16,12 @@ const HEADER_LINE = /^[A-Z][A-Za-z-]{1,15}: .*$/ */ const WORD_CHARACTER = /(?![\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Hangul}\p{sc=Thai}])[\p{L}\p{N}_]/u +const WORD_RUN = new RegExp(`(?:${WORD_CHARACTER.source})+`, 'gu') + +/** The words of a text, split where a run of `WORD_CHARACTER`s ends. */ +export function textWords(text: string): string[] { + return text.match(WORD_RUN) ?? [] +} /** * The document text without the header block some connectors prefix (the diff --git a/apps/sim/lib/sim-search/live/account-session.ts b/apps/sim/lib/sim-search/live/account-session.ts index 8b9f543e4ac..c343e894fd1 100644 --- a/apps/sim/lib/sim-search/live/account-session.ts +++ b/apps/sim/lib/sim-search/live/account-session.ts @@ -23,9 +23,9 @@ import { livePolicyFor, loadLiveSearchPolicies } from '@/lib/sim-search/live/pol import { LIVE_SEARCH_PROVIDER_CATALOG } from '@/lib/sim-search/live/provider-catalog' import { isSearchProviderEnabled } from '@/lib/sim-search/live/provider-rollout' import { - anyTermRetry, previewNativeText, readNativeProvider, + requiresEveryTerm, searchNativeProvider, } from '@/lib/sim-search/live/providers' import { searchWithinPolicy } from '@/lib/sim-search/live/scoped-search' @@ -205,9 +205,8 @@ export async function openLiveAccountSession( client ? searchNativeProvider(provider, client, native) : searchMcp(native) ) } - const retry = client ? anyTermRetry(provider) : undefined - return retry - ? searchWithAnyTermFallback(searchOnce, search, signal, retry) + return client && requiresEveryTerm(provider) + ? searchWithAnyTermFallback(searchOnce, search, signal) : searchOnce(search) }, async verify(document) { diff --git a/apps/sim/lib/sim-search/live/application.test.ts b/apps/sim/lib/sim-search/live/application.test.ts index 0591419d86c..7591c26a748 100644 --- a/apps/sim/lib/sim-search/live/application.test.ts +++ b/apps/sim/lib/sim-search/live/application.test.ts @@ -26,7 +26,7 @@ const mocks = vi.hoisted(() => ({ json: vi.fn(), service: vi.fn(), destroyPool: vi.fn(), - anyTermRetry: vi.fn(), + requiresEveryTerm: vi.fn(), preview: vi.fn(), })) vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) @@ -67,7 +67,7 @@ vi.mock('@/lib/sim-search/live/providers', () => ({ liveSearchGuidance: (providers: string[]) => `Live coverage: ${[...new Set(providers)]}`, searchNativeProvider: mocks.search, readNativeProvider: mocks.read, - anyTermRetry: mocks.anyTermRetry, + requiresEveryTerm: mocks.requiresEveryTerm, previewNativeText: mocks.preview, })) vi.mock('@/executor/utils/resolved-secret-content-projection', () => ({ @@ -140,7 +140,7 @@ describe('authorized live retrieval', () => { documents: [{ ...document, id: 'src/a.ts', container: '42', kind: 'code' }], }) mocks.adminVerify.mockResolvedValue(true) - mocks.anyTermRetry.mockReset().mockReturnValue(undefined) + mocks.requiresEveryTerm.mockReset().mockReturnValue(false) mocks.preview.mockReset().mockResolvedValue(undefined) }) describe('Zoom continuation integrity', () => { @@ -698,7 +698,7 @@ describe('authorized live retrieval', () => { }) it('retries an underfilled search with any term for providers that require every term', async () => { - mocks.anyTermRetry.mockReturnValue({}) + mocks.requiresEveryTerm.mockReturnValue(true) const loose = { ...document, id: 'loose', url: 'https://docs.google.com/document/d/loose/edit' } mocks.search.mockImplementation(async (_provider, _client, search) => ({ documents: search.textMatch === 'any' ? [loose, document] : [document], diff --git a/apps/sim/lib/sim-search/live/atlassian.test.ts b/apps/sim/lib/sim-search/live/atlassian.test.ts index d0d0816ddd5..5ea4fd32e64 100644 --- a/apps/sim/lib/sim-search/live/atlassian.test.ts +++ b/apps/sim/lib/sim-search/live/atlassian.test.ts @@ -197,3 +197,36 @@ describe('Confluence live documents', () => { expect(await previewConfluence(page('

Rollout steps

'), document)).toBe('Rollout steps') }) }) + +describe('Jira any-term search', () => { + const issues = { + '/ex/jira/cloud/rest/api/3/search/jql': { + issues: [ + { key: 'ENG-1', fields: { summary: 'Token creator role request' } }, + { key: 'ENG-2', fields: { summary: 'Role request form' } }, + ], + }, + } + const query = { query: 'glean token creator role request', limit: 25, scopes: [] } + + it('keeps only issues holding at least half the query terms', async () => { + const page = await searchAtlassian(client(issues), 'jira', { ...query, textMatch: 'any' }) + expect(page.documents.map((document) => document.id)).toEqual(['ENG-1']) + }) + + it('keeps every issue an all-terms search returns', async () => { + const page = await searchAtlassian(client(issues), 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['ENG-1', 'ENG-2']) + }) + + it('leaves a Confluence any-term search to its relevance ranking', async () => { + const api = client({ + '/ex/confluence/cloud/wiki/rest/api/search': { + results: [{ content: { id: '5', type: 'page', title: 'Role request form' } }], + _links: {}, + }, + }) + const page = await searchAtlassian(api, 'confluence', { ...query, textMatch: 'any' }) + expect(page.documents.map((document) => document.id)).toEqual(['5']) + }) +}) diff --git a/apps/sim/lib/sim-search/live/atlassian.ts b/apps/sim/lib/sim-search/live/atlassian.ts index b80f4d87079..38dae7fc8bc 100644 --- a/apps/sim/lib/sim-search/live/atlassian.ts +++ b/apps/sim/lib/sim-search/live/atlassian.ts @@ -9,7 +9,11 @@ import { } from '@/lib/sim-search/live/http' import { interleaveByRank } from '@/lib/sim-search/live/pages' import { providerText } from '@/lib/sim-search/live/text' -import { confluenceTextClause, jiraTextClause } from '@/lib/sim-search/live/text-match' +import { + confluenceTextClause, + holdsHalfTheTerms, + jiraTextClause, +} from '@/lib/sim-search/live/text-match' import type { NativeClient, NativeDocument, @@ -152,6 +156,11 @@ export async function searchAtlassian( const direction = dateSortDirection(input.filters) const order = direction ? `${field} ${direction.toUpperCase()}` : undefined const text = nativeText(input) + /** + * Jira's any-term query ORs pairs of terms, which on a large site matches issues sharing two + * common words; like a minimum-should-match, an issue must hold at least half the terms. + */ + const relevant = input.textMatch === 'any' && text ? holdsHalfTheTerms(text) : undefined const pages = await Promise.all( selected.map(async (site) => { const cloudId = string(site.id) @@ -173,7 +182,9 @@ export async function searchAtlassian( }) ) return { - documents: array(data.issues).map((row) => issue(row, cloudId, origin)), + documents: array(data.issues) + .map((row) => issue(row, cloudId, origin)) + .filter((document) => !relevant || relevant(`${document.title}\n${document.content}`)), excluded: false, next: string(data.nextPageToken) || undefined, } diff --git a/apps/sim/lib/sim-search/live/providers.ts b/apps/sim/lib/sim-search/live/providers.ts index 6e987e3ad16..7333304ae55 100644 --- a/apps/sim/lib/sim-search/live/providers.ts +++ b/apps/sim/lib/sim-search/live/providers.ts @@ -20,7 +20,6 @@ import { type LiveSearchProviderId, } from '@/lib/sim-search/live/provider-catalog' import { readSlack, searchSlack } from '@/lib/sim-search/live/slack' -import type { AnyTermFallbackOptions } from '@/lib/sim-search/live/term-fallback' import type { NativeClient, NativeDocument, @@ -47,12 +46,8 @@ interface NativeQueryGuide { interface NativeProvider { guide: NativeQueryGuide - /** - * Plain queries must contain every word, so an underfilled search retries with any term. - * `requireHalfTheTerms` is for a provider whose any-term query ranks items sharing just a couple - * of common words alongside close matches. - */ - anyTermRetry?: AnyTermFallbackOptions + /** Plain queries must contain every word, so an underfilled search retries with any term. */ + requiresEveryTerm?: true /** * Full text for a search preview, for providers whose search returns only an excerpt or * metadata. At most one request; undefined for kinds without a cheap text form. @@ -181,7 +176,7 @@ export const LIVE_SEARCH_PROVIDERS = { avoid: 'JQL with only an ORDER BY clause (Jira rejects unbounded queries), and identifying users other than currentUser() by display name or email; use their account ID.', }, - anyTermRetry: { requireHalfTheTerms: true }, + requiresEveryTerm: true, search: (client, input) => searchAtlassian(client, 'jira', input), read: (client, reference) => readAtlassian(client, 'jira', reference.id, reference.container), }, @@ -195,7 +190,7 @@ export const LIVE_SEARCH_PROVIDERS = { avoid: 'starting the query with a negative clause (NOT, !=, !~ or NOT IN), which CQL rejects; lead with a positive clause such as type IN (page, blogpost).', }, - anyTermRetry: { requireHalfTheTerms: false }, + requiresEveryTerm: true, previewText: previewConfluence, search: (client, input) => searchAtlassian(client, 'confluence', input), read: (client, reference) => @@ -318,13 +313,10 @@ export function searchNativeProvider( return adapter.search(client, input) } -/** - * How the provider retries an underfilled plain search with any term; undefined when its - * plain-text search does not require every query word. - */ -export function anyTermRetry(provider: LiveSearchProviderId): AnyTermFallbackOptions | undefined { +/** True when the provider's plain-text search matches only items containing every query word. */ +export function requiresEveryTerm(provider: LiveSearchProviderId): boolean { const adapter = LIVE_SEARCH_PROVIDERS[provider] - return 'anyTermRetry' in adapter ? adapter.anyTermRetry : undefined + return 'requiresEveryTerm' in adapter && adapter.requiresEveryTerm === true } /** diff --git a/apps/sim/lib/sim-search/live/term-fallback.test.ts b/apps/sim/lib/sim-search/live/term-fallback.test.ts index 4e9220634ad..34c1536789f 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.test.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.test.ts @@ -217,91 +217,6 @@ describe('searchWithAnyTermFallback', () => { expect(search).toHaveBeenCalledTimes(1) }) - describe('when partial matches must contain half the terms', () => { - const long: NativeSearchInput = { - query: 'glean token creator role request', - limit: 10, - scopes: [], - } - const halfTheTerms = { requireHalfTheTerms: true } - - it('drops a partial match containing fewer than half the terms', async () => { - const search = provider( - page(), - page( - { id: 'two-terms', title: 'Role request form' }, - { id: 'three-terms', title: 'Token creator role' }, - { id: 'in-description', title: 'Access', content: 'Glean token request for my team' } - ) - ) - const result = await searchWithAnyTermFallback(search, long, live, halfTheTerms) - expect(result.documents.map((document) => document.id)).toEqual([ - 'three-terms', - 'in-description', - ]) - }) - - it('counts a word and its plural as one term, as the provider matches them', async () => { - const search = provider( - page(), - page({ id: 'plurals', title: 'Glean tokens', content: 'Creators file requests' }) - ) - const result = await searchWithAnyTermFallback(search, long, live, halfTheTerms) - expect(result.documents.map((document) => document.id)).toEqual(['plurals']) - }) - - it('counts a singular word whose plural adds -es, in either form', async () => { - const search = provider( - page(), - page( - { id: 'singular', title: 'Database release' }, - { id: 'plural', title: 'Databases', content: 'Weekly releases' } - ) - ) - const result = await searchWithAnyTermFallback( - search, - { query: 'database release checklist owner', limit: 10, scopes: [] }, - live, - halfTheTerms - ) - expect(result.documents.map((document) => document.id).sort()).toEqual(['plural', 'singular']) - }) - - it('counts a term with punctuation inside it, such as ci/cd', async () => { - const search = provider(page(), page({ id: 'ci', title: 'CI/CD pipeline' })) - const result = await searchWithAnyTermFallback( - search, - { query: 'ci/cd pipeline failure owner', limit: 10, scopes: [] }, - live, - halfTheTerms - ) - expect(result.documents.map((document) => document.id)).toEqual(['ci']) - }) - - it('returns the all-terms page unchanged when no partial match is close enough', async () => { - const strict = page({ id: 'exact', title: 'Glean token creator role request' }) - const search = provider(strict, page({ id: 'loose', title: 'Role request' })) - expect(await searchWithAnyTermFallback(search, long, live, halfTheTerms)).toBe(strict) - }) - - it('keeps a single matching term enough for a two-term query', async () => { - const search = provider(page(), page({ id: 'one', title: 'Contractor directory' })) - const result = await searchWithAnyTermFallback( - search, - { query: 'contractor offboarding', limit: 4, scopes: [] }, - live, - halfTheTerms - ) - expect(result.documents.map((document) => document.id)).toEqual(['one']) - }) - - it('leaves providers without the floor adding loose partial matches', async () => { - const search = provider(page(), page({ id: 'two-terms', title: 'Role request form' })) - const result = await searchWithAnyTermFallback(search, long, live) - expect(result.documents.map((document) => document.id)).toEqual(['two-terms']) - }) - }) - it('leaves the page untouched when the any-term search adds nothing new', async () => { const strict = page({ id: 'exact' }) const search = provider(strict, page({ id: 'exact' })) diff --git a/apps/sim/lib/sim-search/live/term-fallback.ts b/apps/sim/lib/sim-search/live/term-fallback.ts index c21edf7a72c..b688e937a0d 100644 --- a/apps/sim/lib/sim-search/live/term-fallback.ts +++ b/apps/sim/lib/sim-search/live/term-fallback.ts @@ -3,11 +3,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { dateSortDirection } from '@/lib/sim-search/live/dates' import { NativeSearchError } from '@/lib/sim-search/live/http' import { joinMessages } from '@/lib/sim-search/live/pages' -import { - containsHalfTheTerms, - significantTerms, - termCoverage, -} from '@/lib/sim-search/live/text-match' +import { significantTerms, termCoverage } from '@/lib/sim-search/live/text-match' import type { NativeDocument, NativePage, NativeSearchInput } from '@/lib/sim-search/live/types' /** @@ -46,20 +42,11 @@ const documentKey = ({ container, kind, id }: NativeDocument) => * slow OR query is cancelled rather than awaited. Whenever it fails short of the caller's own * cancellation (a provider refusal, the budget running out, a transport error), the all-terms page * is kept, reported as partial. Cancellation through `signal` propagates like the first search. - * - * With `requireHalfTheTerms`, a partial match must contain at least half the query's terms in its - * title or text, for providers whose OR query matches items sharing only a couple of common words. */ -export interface AnyTermFallbackOptions { - /** Keep only partial matches holding at least half the query's terms. */ - requireHalfTheTerms?: boolean -} - export async function searchWithAnyTermFallback( search: (input: NativeSearchInput) => Promise, input: NativeSearchInput, - signal: AbortSignal, - options: AnyTermFallbackOptions = {} + signal: AbortSignal ): Promise { const strict = await search(input) const terms = significantTerms(input.query) @@ -106,12 +93,7 @@ export async function searchWithAnyTermFallback( } const seen = new Set(strict.documents.map(documentKey)) const additions = relaxed.documents - .filter( - (document) => - !seen.has(documentKey(document)) && - (!options.requireHalfTheTerms || - containsHalfTheTerms(`${document.title}\n${document.content}`, input.query)) - ) + .filter((document) => !seen.has(documentKey(document))) .map((document, rank) => ({ document, rank, diff --git a/apps/sim/lib/sim-search/live/text-match.test.ts b/apps/sim/lib/sim-search/live/text-match.test.ts index acf083fadea..73940d3538b 100644 --- a/apps/sim/lib/sim-search/live/text-match.test.ts +++ b/apps/sim/lib/sim-search/live/text-match.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { confluenceTextClause, jiraTextClause } from '@/lib/sim-search/live/text-match' +import { + confluenceTextClause, + holdsHalfTheTerms, + jiraTextClause, +} from '@/lib/sim-search/live/text-match' describe('text match clauses', () => { it('matches either term of a two-term query, leaving out stopwords', () => { @@ -57,4 +61,29 @@ describe('text match clauses', () => { it('falls back to the all-terms clause when fewer than two terms are distinctive', () => { expect(jiraTextClause('how does it work', 'any')).toBe('text ~ "how does it work"') }) + + describe('holdsHalfTheTerms', () => { + it('needs at least half the distinct terms, rounding up', () => { + const holds = holdsHalfTheTerms('glean token creator role request') + expect(holds('Role request form')).toBe(false) + expect(holds('Token creator role')).toBe(true) + }) + + it('counts a word and its plural as one term in either direction', () => { + expect(holdsHalfTheTerms('glean token creator role request')('Glean tokens, creators')).toBe( + true + ) + const holds = holdsHalfTheTerms('database release checklist owner') + expect(holds('Database release')).toBe(true) + expect(holds('Databases and weekly releases')).toBe(true) + }) + + it('matches a term with punctuation inside it as written', () => { + expect(holdsHalfTheTerms('ci/cd pipeline failure owner')('CI/CD pipeline')).toBe(true) + }) + + it('accepts one matching term for a two-term query', () => { + expect(holdsHalfTheTerms('contractor offboarding')('Contractor directory')).toBe(true) + }) + }) }) diff --git a/apps/sim/lib/sim-search/live/text-match.ts b/apps/sim/lib/sim-search/live/text-match.ts index 494d5dfb103..a4a11419e24 100644 --- a/apps/sim/lib/sim-search/live/text-match.ts +++ b/apps/sim/lib/sim-search/live/text-match.ts @@ -1,4 +1,4 @@ -import { findTermMatches, queryTerms } from '@/lib/knowledge/search/snippet' +import { findTermMatches, queryTerms, textWords } from '@/lib/knowledge/search/snippet' /** * How a plain query's words must match a document. Confluence and Jira text search require every @@ -117,15 +117,19 @@ export function termCoverage(text: string, terms: readonly string[]): number { } /** - * Whether the text holds at least half the query's distinct terms. Query and text words are - * compared by their singular key, so a word and its regular plural count as one term as a stemmed - * search matches them; a term with punctuation inside it is matched as written. + * A test for whether a text holds at least half the query's distinct terms. Query and text words + * are compared by their singular key, so a word and its regular plural count as one term as a + * stemmed search matches them; a term with punctuation inside it is matched as written. */ -export function containsHalfTheTerms(text: string, query: string): boolean { +export function holdsHalfTheTerms(query: string): (text: string) => boolean { const terms = distinctTerms(query, singularKey) - const words = new Set(text.match(/[\p{L}\p{N}_]+/gu)?.map(singularKey)) - const present = terms.filter( - (term) => words.has(singularKey(term)) || findTermMatches(text, [term]).length > 0 - ) - return present.length >= Math.ceil(terms.length / 2) + const keys = terms.map(singularKey) + const required = Math.ceil(terms.length / 2) + return (text) => { + const words = new Set(textWords(text).map(singularKey)) + const present = terms.filter( + (term, index) => words.has(keys[index]) || findTermMatches(text, [term]).length > 0 + ) + return present.length >= required + } } From b943aa5a4a6ce3481f00bb1ae61f5ee855e699f8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 14:03:25 -0700 Subject: [PATCH 5/6] chore(search): terse floor comment, guard the query-side plural fold --- apps/sim/lib/sim-search/live/atlassian.ts | 6 ++---- apps/sim/lib/sim-search/live/text-match.test.ts | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/sim-search/live/atlassian.ts b/apps/sim/lib/sim-search/live/atlassian.ts index 38dae7fc8bc..fa7748eb299 100644 --- a/apps/sim/lib/sim-search/live/atlassian.ts +++ b/apps/sim/lib/sim-search/live/atlassian.ts @@ -156,10 +156,8 @@ export async function searchAtlassian( const direction = dateSortDirection(input.filters) const order = direction ? `${field} ${direction.toUpperCase()}` : undefined const text = nativeText(input) - /** - * Jira's any-term query ORs pairs of terms, which on a large site matches issues sharing two - * common words; like a minimum-should-match, an issue must hold at least half the terms. - */ + // Jira's any-term JQL matches issues sharing two common words; like minimum-should-match, + // keep only issues holding at least half the terms. const relevant = input.textMatch === 'any' && text ? holdsHalfTheTerms(text) : undefined const pages = await Promise.all( selected.map(async (site) => { diff --git a/apps/sim/lib/sim-search/live/text-match.test.ts b/apps/sim/lib/sim-search/live/text-match.test.ts index 73940d3538b..928d830d37d 100644 --- a/apps/sim/lib/sim-search/live/text-match.test.ts +++ b/apps/sim/lib/sim-search/live/text-match.test.ts @@ -73,9 +73,10 @@ describe('text match clauses', () => { expect(holdsHalfTheTerms('glean token creator role request')('Glean tokens, creators')).toBe( true ) - const holds = holdsHalfTheTerms('database release checklist owner') - expect(holds('Database release')).toBe(true) - expect(holds('Databases and weekly releases')).toBe(true) + expect( + holdsHalfTheTerms('database release checklist owner')('Databases and weekly releases') + ).toBe(true) + expect(holdsHalfTheTerms('databases releases checklist owner')('Database release')).toBe(true) }) it('matches a term with punctuation inside it as written', () => { From 54018887881082814f3cc073195c235281afa215 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 13 Oct 2026 14:08:42 -0700 Subject: [PATCH 6/6] fix(search): measure Jira partial-match coverage on the summary and description only The issue mapper appends a Status line, so a query containing 'status' gained a term from the label alone. The floor now reads the raw summary and description fields before mapping. --- apps/sim/lib/sim-search/live/atlassian.test.ts | 15 +++++++++++++++ apps/sim/lib/sim-search/live/atlassian.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/sim-search/live/atlassian.test.ts b/apps/sim/lib/sim-search/live/atlassian.test.ts index 5ea4fd32e64..4168a50fcfb 100644 --- a/apps/sim/lib/sim-search/live/atlassian.test.ts +++ b/apps/sim/lib/sim-search/live/atlassian.test.ts @@ -214,6 +214,21 @@ describe('Jira any-term search', () => { expect(page.documents.map((document) => document.id)).toEqual(['ENG-1']) }) + it('counts only the summary and description, not the added status line', async () => { + const api = client({ + '/ex/jira/cloud/rest/api/3/search/jql': { + issues: [{ key: 'ENG-3', fields: { summary: 'Release update', status: { name: 'Open' } } }], + }, + }) + const page = await searchAtlassian(api, 'jira', { + query: 'release status update owner timeline', + limit: 25, + scopes: [], + textMatch: 'any', + }) + expect(page.documents).toEqual([]) + }) + it('keeps every issue an all-terms search returns', async () => { const page = await searchAtlassian(client(issues), 'jira', query) expect(page.documents.map((document) => document.id)).toEqual(['ENG-1', 'ENG-2']) diff --git a/apps/sim/lib/sim-search/live/atlassian.ts b/apps/sim/lib/sim-search/live/atlassian.ts index fa7748eb299..86721de66c2 100644 --- a/apps/sim/lib/sim-search/live/atlassian.ts +++ b/apps/sim/lib/sim-search/live/atlassian.ts @@ -181,8 +181,14 @@ export async function searchAtlassian( ) return { documents: array(data.issues) - .map((row) => issue(row, cloudId, origin)) - .filter((document) => !relevant || relevant(`${document.title}\n${document.content}`)), + .filter((row) => { + const fields = object(row.fields) + return ( + !relevant || + relevant(`${string(fields.summary)}\n${textContent(fields.description)}`) + ) + }) + .map((row) => issue(row, cloudId, origin)), excluded: false, next: string(data.nextPageToken) || undefined, }