Skip to content

Commit 74b96a9

Browse files
committed
fix(knowledge): document tag filter matches case-insensitively and by calendar day
The Knowledge Base document list applied tag filters with case-sensitive text equality and compared date tags against a midnight-UTC timestamp, so text filters missed on any casing difference and date eq never matched a stored timestamp — both silently returned empty results. Align the document-list filter with the knowledge search filter semantics: text eq/neq are now case-insensitive (LOWER) and date comparisons run on the calendar day (::date). Extract the predicate builder into its own single-responsibility module with unit coverage, and tidy the filter popover's secondary labels to the caption text size for chip-design consistency.
1 parent a1d5870 commit 74b96a9

5 files changed

Lines changed: 341 additions & 161 deletions

File tree

apps/sim/app/api/knowledge/[id]/documents/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {
2323
getProcessingConfig,
2424
KnowledgeBaseFileOwnershipError,
2525
processDocumentsWithQueue,
26-
type TagFilterCondition,
2726
} from '@/lib/knowledge/documents/service'
27+
import type { TagFilterCondition } from '@/lib/knowledge/documents/tag-filter'
2828
import { captureServerEvent } from '@/lib/posthog/server'
2929
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
3030

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ export function KnowledgeBase({
926926
setSelectedDocuments(new Set())
927927
setIsSelectAllMode(false)
928928
}}
929-
className='-mr-1 h-auto px-1 py-0.5 text-[var(--text-muted)] text-xs hover-hover:text-[var(--text-secondary)]'
929+
className='-mr-1 h-auto px-1 py-0.5 text-[var(--text-muted)] text-caption hover-hover:text-[var(--text-secondary)]'
930930
>
931931
Clear
932932
</Button>
@@ -1499,7 +1499,7 @@ function TagFilterValueControl({ entry, onChange }: TagFilterValueControlProps)
14991499
fullWidth
15001500
flush
15011501
/>
1502-
<span className='flex-shrink-0 text-[var(--text-muted)] text-xs'>to</span>
1502+
<span className='flex-shrink-0 text-[var(--text-muted)] text-caption'>to</span>
15031503
<ChipDatePicker
15041504
value={entry.valueTo || undefined}
15051505
onChange={(value) => onChange({ valueTo: value })}
@@ -1530,7 +1530,7 @@ function TagFilterValueControl({ entry, onChange }: TagFilterValueControlProps)
15301530
onChange={(event) => onChange({ value: event.target.value })}
15311531
placeholder='From'
15321532
/>
1533-
<span className='flex-shrink-0 text-[var(--text-muted)] text-xs'>to</span>
1533+
<span className='flex-shrink-0 text-[var(--text-muted)] text-caption'>to</span>
15341534
<ChipInput
15351535
value={entry.valueTo}
15361536
onChange={(event) => onChange({ valueTo: event.target.value })}
@@ -1625,7 +1625,7 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio
16251625
{activeCount > 0 && (
16261626
<Button
16271627
variant='ghost'
1628-
className='-mr-1 h-auto px-1 py-0.5 text-[var(--text-muted)] text-xs hover-hover:text-[var(--text-secondary)]'
1628+
className='-mr-1 h-auto px-1 py-0.5 text-[var(--text-muted)] text-caption hover-hover:text-[var(--text-secondary)]'
16291629
onClick={() => onChange([])}
16301630
>
16311631
Clear all
@@ -1648,7 +1648,7 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio
16481648
<div key={entry.id} className='flex flex-col gap-2'>
16491649
{index > 0 && (
16501650
<div className='flex items-center gap-2'>
1651-
<span className='shrink-0 text-[var(--text-muted)] text-xs leading-none'>
1651+
<span className='shrink-0 text-[var(--text-muted)] text-caption leading-none'>
16521652
and
16531653
</span>
16541654
<div className='h-px flex-1 bg-[var(--border-1)]' />

apps/sim/lib/knowledge/documents/service.ts

Lines changed: 5 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,7 @@ import { sha256Hex } from '@sim/security/hash'
1212
import { getErrorMessage, toError } from '@sim/utils/errors'
1313
import { generateId } from '@sim/utils/id'
1414
import { tasks } from '@trigger.dev/sdk'
15-
import {
16-
and,
17-
asc,
18-
desc,
19-
eq,
20-
gt,
21-
gte,
22-
inArray,
23-
isNotNull,
24-
isNull,
25-
lt,
26-
lte,
27-
ne,
28-
type SQL,
29-
sql,
30-
} from 'drizzle-orm'
15+
import { and, asc, desc, eq, inArray, isNotNull, isNull, type SQL, sql } from 'drizzle-orm'
3116
import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor'
3217
import { recordUsage } from '@/lib/billing/core/usage-log'
3318
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
@@ -36,6 +21,10 @@ import { resolveTriggerRegion } from '@/lib/core/async-jobs/region'
3621
import { env, envNumber } from '@/lib/core/config/env'
3722
import { getCostMultiplier, isTriggerDevEnabled } from '@/lib/core/config/env-flags'
3823
import { processDocument } from '@/lib/knowledge/documents/document-processor'
24+
import {
25+
buildTagFilterCondition,
26+
type TagFilterCondition,
27+
} from '@/lib/knowledge/documents/tag-filter'
3928
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
4029
import { getEmbeddingModelInfo } from '@/lib/knowledge/embedding-models'
4130
import { generateEmbeddings } from '@/lib/knowledge/embeddings'
@@ -997,145 +986,6 @@ export async function createDocumentRecords(
997986
})
998987
}
999988

1000-
export interface TagFilterCondition {
1001-
tagSlot: string
1002-
fieldType: 'text' | 'number' | 'date' | 'boolean'
1003-
operator: string
1004-
value: unknown
1005-
valueTo?: unknown
1006-
}
1007-
1008-
const ALLOWED_TAG_SLOTS = new Set([
1009-
'tag1',
1010-
'tag2',
1011-
'tag3',
1012-
'tag4',
1013-
'tag5',
1014-
'tag6',
1015-
'tag7',
1016-
'number1',
1017-
'number2',
1018-
'number3',
1019-
'number4',
1020-
'number5',
1021-
'date1',
1022-
'date2',
1023-
'boolean1',
1024-
'boolean2',
1025-
'boolean3',
1026-
])
1027-
1028-
function escapeLikePattern(s: string): string {
1029-
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
1030-
}
1031-
1032-
function buildTagFilterCondition(filter: TagFilterCondition): SQL | undefined {
1033-
if (!ALLOWED_TAG_SLOTS.has(filter.tagSlot)) return undefined
1034-
1035-
const col = document[filter.tagSlot as keyof typeof document]
1036-
1037-
if (filter.fieldType === 'text') {
1038-
const v = String(filter.value ?? '')
1039-
switch (filter.operator) {
1040-
case 'eq':
1041-
return eq(col as typeof document.tag1, v)
1042-
case 'neq':
1043-
return ne(col as typeof document.tag1, v)
1044-
case 'contains': {
1045-
const escaped = escapeLikePattern(v)
1046-
return sql`LOWER(${col}) LIKE LOWER(${`%${escaped}%`}) ESCAPE '\\'`
1047-
}
1048-
case 'not_contains': {
1049-
const escaped = escapeLikePattern(v)
1050-
return sql`LOWER(${col}) NOT LIKE LOWER(${`%${escaped}%`}) ESCAPE '\\'`
1051-
}
1052-
case 'starts_with': {
1053-
const escaped = escapeLikePattern(v)
1054-
return sql`LOWER(${col}) LIKE LOWER(${`${escaped}%`}) ESCAPE '\\'`
1055-
}
1056-
case 'ends_with': {
1057-
const escaped = escapeLikePattern(v)
1058-
return sql`LOWER(${col}) LIKE LOWER(${`%${escaped}`}) ESCAPE '\\'`
1059-
}
1060-
default:
1061-
return undefined
1062-
}
1063-
}
1064-
1065-
if (filter.fieldType === 'number') {
1066-
const num = Number(filter.value)
1067-
if (Number.isNaN(num)) return undefined
1068-
switch (filter.operator) {
1069-
case 'eq':
1070-
return eq(col as typeof document.number1, num)
1071-
case 'neq':
1072-
return ne(col as typeof document.number1, num)
1073-
case 'gt':
1074-
return gt(col as typeof document.number1, num)
1075-
case 'gte':
1076-
return gte(col as typeof document.number1, num)
1077-
case 'lt':
1078-
return lt(col as typeof document.number1, num)
1079-
case 'lte':
1080-
return lte(col as typeof document.number1, num)
1081-
case 'between': {
1082-
const numTo = Number(filter.valueTo)
1083-
if (Number.isNaN(numTo)) return undefined
1084-
return and(
1085-
gte(col as typeof document.number1, num),
1086-
lte(col as typeof document.number1, numTo)
1087-
)
1088-
}
1089-
default:
1090-
return undefined
1091-
}
1092-
}
1093-
1094-
if (filter.fieldType === 'date') {
1095-
const v = String(filter.value ?? '')
1096-
switch (filter.operator) {
1097-
case 'eq':
1098-
return eq(col as typeof document.date1, new Date(v))
1099-
case 'neq':
1100-
return ne(col as typeof document.date1, new Date(v))
1101-
case 'gt':
1102-
return gt(col as typeof document.date1, new Date(v))
1103-
case 'gte':
1104-
return gte(col as typeof document.date1, new Date(v))
1105-
case 'lt':
1106-
return lt(col as typeof document.date1, new Date(v))
1107-
case 'lte':
1108-
return lte(col as typeof document.date1, new Date(v))
1109-
case 'between': {
1110-
if (!filter.valueTo) return undefined
1111-
const valueTo = String(filter.valueTo)
1112-
return and(
1113-
gte(col as typeof document.date1, new Date(v)),
1114-
lte(col as typeof document.date1, new Date(valueTo))
1115-
)
1116-
}
1117-
default:
1118-
return undefined
1119-
}
1120-
}
1121-
1122-
if (filter.fieldType === 'boolean') {
1123-
const boolVal =
1124-
typeof filter.value === 'boolean' ? filter.value : parseBooleanValue(String(filter.value))
1125-
if (boolVal === null) return undefined
1126-
switch (filter.operator) {
1127-
case 'eq':
1128-
return eq(col as typeof document.boolean1, boolVal)
1129-
case 'neq':
1130-
return ne(col as typeof document.boolean1, boolVal)
1131-
default:
1132-
return undefined
1133-
}
1134-
}
1135-
1136-
return undefined
1137-
}
1138-
1139989
export async function getDocuments(
1140990
knowledgeBaseId: string,
1141991
options: {

0 commit comments

Comments
 (0)