Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4f5f140
creating boolean, number and date tags with different equality matchings
Dec 17, 2025
3432a10
feat: add UI for tag field types with filter operators
Dec 17, 2025
3390f64
feat: add field type support to document-tag-entry component
Dec 17, 2025
511d891
fix: resolve MAX_TAG_SLOTS error and z-index dropdown issue
Dec 17, 2025
030d8b6
fix: handle non-text columns in getTagUsage query
Dec 17, 2025
38f46bd
refactor: use EMCN components for KB UI
Dec 17, 2025
c23d6db
fix: layout for delete button next to date picker
Dec 17, 2025
21174d8
fix: clear value when switching tag field type
Dec 17, 2025
dab6383
feat: add full support for number/date/boolean tag filtering in KB se…
Dec 17, 2025
d44b1af
fixing tags import issue
Dec 18, 2025
c9c6ba9
fix rm file
Dec 18, 2025
1612a8a
reduced number to 3 and date to 2
Dec 18, 2025
1725ced
fixing lint
Dec 18, 2025
78bbedd
fixed the prop size issue
Dec 18, 2025
523fc5e
increased number from 3 to 5 and boolean from 7 to 2
Dec 18, 2025
4d83cb7
fixed number the sql stuff
Dec 18, 2025
22745f7
progress
icecrasher321 Dec 19, 2025
d5fd88d
fix document tag and kb tag modals
icecrasher321 Dec 19, 2025
3e49031
update datepicker emcn component
icecrasher321 Dec 19, 2025
44c513c
fix ui
icecrasher321 Dec 19, 2025
bbc69a6
progress on KB block tags UI
icecrasher321 Dec 20, 2025
869ad55
fix issues with date filters
icecrasher321 Dec 20, 2025
ca59aae
fix execution parsing of types for KB tags
icecrasher321 Dec 20, 2025
90e32df
remove migration before merge
icecrasher321 Dec 20, 2025
8c6dd3a
Merge branch 'staging' into feat/filter-tags-update
icecrasher321 Dec 20, 2025
dda74b9
regen migrations
icecrasher321 Dec 20, 2025
51f1ce1
fix tests and types
icecrasher321 Dec 20, 2025
eae7f7b
address greptile comments
icecrasher321 Dec 20, 2025
1d10772
fix more greptile comments
icecrasher321 Dec 20, 2025
8a91eaf
fix filtering logic for multiple of same row
icecrasher321 Dec 20, 2025
998859d
fix tests
icecrasher321 Dec 20, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,38 @@ export async function POST(
const validatedData = CreateChunkSchema.parse(searchParams)

const docTags = {
// Text tags
tag1: doc.tag1 ?? null,
tag2: doc.tag2 ?? null,
tag3: doc.tag3 ?? null,
tag4: doc.tag4 ?? null,
tag5: doc.tag5 ?? null,
tag6: doc.tag6 ?? null,
tag7: doc.tag7 ?? null,
// Number tags
number1: doc.number1 ?? null,
number2: doc.number2 ?? null,
number3: doc.number3 ?? null,
number4: doc.number4 ?? null,
number5: doc.number5 ?? null,
number6: doc.number6 ?? null,
number7: doc.number7 ?? null,
// Date tags
date1: doc.date1 ?? null,
date2: doc.date2 ?? null,
date3: doc.date3 ?? null,
date4: doc.date4 ?? null,
date5: doc.date5 ?? null,
date6: doc.date6 ?? null,
date7: doc.date7 ?? null,
// Boolean tags
boolean1: doc.boolean1 ?? null,
boolean2: doc.boolean2 ?? null,
boolean3: doc.boolean3 ?? null,
boolean4: doc.boolean4 ?? null,
boolean5: doc.boolean5 ?? null,
boolean6: doc.boolean6 ?? null,
boolean7: doc.boolean7 ?? null,
}

const newChunk = await createChunk(
Expand Down
96 changes: 85 additions & 11 deletions apps/sim/app/api/knowledge/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { generateRequestId } from '@/lib/core/utils/request'
import { TAG_SLOTS } from '@/lib/knowledge/constants'
import { ALL_TAG_SLOTS } from '@/lib/knowledge/constants'
import { getDocumentTagDefinitions } from '@/lib/knowledge/tags/service'
import { createLogger } from '@/lib/logs/console/logger'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
Expand All @@ -20,6 +20,16 @@ import { calculateCost } from '@/providers/utils'

const logger = createLogger('VectorSearchAPI')

/** Structured tag filter with operator support */
const StructuredTagFilterSchema = z.object({
tagName: z.string(),
tagSlot: z.string().optional(),
fieldType: z.enum(['text', 'number', 'date', 'boolean']).default('text'),
operator: z.string().default('eq'),
value: z.union([z.string(), z.number(), z.boolean()]),
valueTo: z.union([z.string(), z.number()]).optional(),
})

const VectorSearchSchema = z
.object({
knowledgeBaseIds: z.union([
Expand All @@ -43,14 +53,20 @@ const VectorSearchSchema = z
.record(z.string())
.optional()
.nullable()
.transform((val) => val || undefined), // Allow dynamic filter keys (display names)
.transform((val) => val || undefined), // Legacy format: simple key-value pairs
tagFilters: z
.array(StructuredTagFilterSchema)
.optional()
.nullable()
.transform((val) => val || undefined), // New format: structured filters with operators
})
.refine(
(data) => {
// Ensure at least query or filters are provided
const hasQuery = data.query && data.query.trim().length > 0
const hasFilters = data.filters && Object.keys(data.filters).length > 0
return hasQuery || hasFilters
const hasLegacyFilters = data.filters && Object.keys(data.filters).length > 0
const hasTagFilters = data.tagFilters && data.tagFilters.length > 0
return hasQuery || hasLegacyFilters || hasTagFilters
},
{
message: 'Please provide either a search query or tag filters to search your knowledge base',
Expand Down Expand Up @@ -89,6 +105,54 @@ export async function POST(request: NextRequest) {

// Map display names to tag slots for filtering
let mappedFilters: Record<string, string> = {}
let structuredFilters: Array<{
tagSlot: string
fieldType: string
operator: string
value: string | number | boolean
valueTo?: string | number
}> = []

// Handle new structured tagFilters format
if (validatedData.tagFilters && accessibleKbIds.length > 0) {
try {
const kbId = accessibleKbIds[0]
const tagDefs = await getDocumentTagDefinitions(kbId)

// Create mapping from display name to tag slot and fieldType
const displayNameToTagDef: Record<string, { tagSlot: string; fieldType: string }> = {}
tagDefs.forEach((def) => {
displayNameToTagDef[def.displayName] = {
tagSlot: def.tagSlot,
fieldType: def.fieldType,
}
})

structuredFilters = validatedData.tagFilters.map((filter) => {
const tagDef = displayNameToTagDef[filter.tagName]
const tagSlot = filter.tagSlot || tagDef?.tagSlot || filter.tagName
const fieldType = filter.fieldType || tagDef?.fieldType || 'text'

logger.debug(
`[${requestId}] Structured filter: ${filter.tagName} -> ${tagSlot} (${fieldType}) ${filter.operator} ${filter.value}`
)

return {
tagSlot,
fieldType,
operator: filter.operator,
value: filter.value,
valueTo: filter.valueTo,
}
})

logger.debug(`[${requestId}] Processed ${structuredFilters.length} structured filters`)
} catch (error) {
logger.error(`[${requestId}] Structured filter processing error:`, error)
}
}

// Handle legacy filters format (for backwards compatibility)
if (validatedData.filters && accessibleKbIds.length > 0) {
try {
// Fetch tag definitions for the first accessible KB (since we're using single KB now)
Expand Down Expand Up @@ -155,26 +219,36 @@ export async function POST(request: NextRequest) {

let results: SearchResult[]

const hasFilters = mappedFilters && Object.keys(mappedFilters).length > 0
const hasLegacyFilters = mappedFilters && Object.keys(mappedFilters).length > 0
const hasStructuredFilters = structuredFilters && structuredFilters.length > 0
const hasFilters = hasLegacyFilters || hasStructuredFilters

if (!hasQuery && hasFilters) {
// Tag-only search without vector similarity
logger.debug(`[${requestId}] Executing tag-only search with filters:`, mappedFilters)
logger.debug(
`[${requestId}] Executing tag-only search with filters:`,
hasStructuredFilters ? structuredFilters : mappedFilters
)
results = await handleTagOnlySearch({
knowledgeBaseIds: accessibleKbIds,
topK: validatedData.topK,
filters: mappedFilters,
filters: hasLegacyFilters ? mappedFilters : undefined,
structuredFilters: hasStructuredFilters ? structuredFilters : undefined,
})
} else if (hasQuery && hasFilters) {
// Tag + Vector search
logger.debug(`[${requestId}] Executing tag + vector search with filters:`, mappedFilters)
logger.debug(
`[${requestId}] Executing tag + vector search with filters:`,
hasStructuredFilters ? structuredFilters : mappedFilters
)
const strategy = getQueryStrategy(accessibleKbIds.length, validatedData.topK)
const queryVector = JSON.stringify(await queryEmbeddingPromise)

results = await handleTagAndVectorSearch({
knowledgeBaseIds: accessibleKbIds,
topK: validatedData.topK,
filters: mappedFilters,
filters: hasLegacyFilters ? mappedFilters : undefined,
structuredFilters: hasStructuredFilters ? structuredFilters : undefined,
queryVector,
distanceThreshold: strategy.distanceThreshold,
})
Expand Down Expand Up @@ -257,9 +331,9 @@ export async function POST(request: NextRequest) {
// Create tags object with display names
const tags: Record<string, any> = {}

TAG_SLOTS.forEach((slot) => {
ALL_TAG_SLOTS.forEach((slot) => {
const tagValue = (result as any)[slot]
if (tagValue) {
if (tagValue !== null && tagValue !== undefined) {
const displayName = kbTagMap[slot] || slot
logger.debug(
`[${requestId}] Mapping ${slot}="${tagValue}" -> "${displayName}"="${tagValue}"`
Expand Down
Loading
Loading