Skip to content

Commit 535f052

Browse files
committed
chore(search): partition dormant indexed org search behind a single gate
Live Search is the only active Sim Search backend. The indexed backend (retrieval over is_search_index knowledge bases) stays in the tree for a possible re-enable, so it now lives in one dormant module behind one switch. - Move the exclusively-indexed code into apps/sim/lib/sim-search/indexed/ with git mv: scoped owner search, indexed document reads, Tin keyword ranking, and the projection-fill probe. Shared code (projections, projector, member/ACL connector machinery, source overview/setup, prewarm) stays where it is. README documents the dormant state, the gate, re-enable steps, and the DB objects it depends on. - Add isIndexedOrgSearchEnabled() in indexed/gate.ts (the inverse of SIM_SEARCH_LIVE) and use it at every indexed branch: the internal search route, MCP tools, Sim's search/read tools, the indexed document page, shared retrieval, and connector indexing policy. - Add check:indexed-org-search-boundary (runs in check:audits): outside the directory only allowlisted entry files may import the use-case or retrieval barrel, each must import the gate, and no deep imports. - Refuse a search that names a search-index knowledge base while dormant (Knowledge block, internal, v1, v2) with SearchIndexDormantError -> 409 "This search index is inactive; use Sim Search." Workspace knowledge bases are unaffected. - While dormant, the projector writes no embedding_keyword_tin rows and the source/ACL fill passes over search-index rows; the decision is passed into packages/db from the app caller. Workspace projection is unchanged. - Cap the projection-fill probe at 250 ms of the leg budget and remember an unknown answer as unfilled for 5 s instead of re-probing every search.
1 parent 316fea0 commit 535f052

56 files changed

Lines changed: 1065 additions & 127 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/api/knowledge/search/route.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
77

88
const mocks = vi.hoisted(() => ({ search: vi.fn() }))
9-
vi.mock('@/lib/knowledge/application/workspace-search', () => ({
9+
vi.mock('@/lib/sim-search/indexed', () => ({
1010
searchScopedKnowledge: { operation: { id: 'knowledge.search' }, execute: mocks.search },
1111
}))
1212

‎apps/sim/app/api/knowledge/search/route.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import {
44
internalRateLimits,
55
internalSessionAuth,
66
} from '@/lib/api/server/routes'
7-
import { isLiveEnterpriseSearchEnabled } from '@/lib/core/config/env-flags'
87
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
98
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
10-
import { searchScopedKnowledge } from '@/lib/knowledge/application/workspace-search'
119
import { DEFAULT_RERANKER_MODEL } from '@/lib/knowledge/reranker-models'
1210
import { sourceAuthor } from '@/lib/knowledge/search/author'
11+
import { searchScopedKnowledge } from '@/lib/sim-search/indexed'
12+
import { isIndexedOrgSearchEnabled } from '@/lib/sim-search/indexed/gate'
1313
import { searchLiveKnowledge } from '@/lib/sim-search/live/application'
1414

1515
const DIRECT_SEARCH_VECTOR_BUDGET_MS = 3000
@@ -81,4 +81,5 @@ const liveSearchRoute = defineInternalJsonRoute({
8181
present: (data) => ({ success: true as const, data }),
8282
})
8383

84-
export const POST = isLiveEnterpriseSearchEnabled ? liveSearchRoute : indexedSearchRoute
84+
/** Indexed organization search is dormant unless its gate is on; Live Search serves otherwise. */
85+
export const POST = isIndexedOrgSearchEnabled() ? indexedSearchRoute : liveSearchRoute

‎apps/sim/app/api/v1/knowledge/search/route.test.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
* @vitest-environment node
88
*/
99

10-
import { createMockRequest, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns } from '@sim/testing'
10+
import {
11+
createMockRequest,
12+
knowledgeApiUtilsMock,
13+
knowledgeApiUtilsMockFns,
14+
resetEnvFlagsMock,
15+
setEnvFlags,
16+
} from '@sim/testing'
1117
import { getErrorMessage } from '@sim/utils/errors'
1218
import { beforeEach, describe, expect, it, vi } from 'vitest'
1319

@@ -118,6 +124,7 @@ const baseKb = (id: string, embeddingModel: string, embeddingDimension = 1536) =
118124
describe('v1 knowledge search route — per-KB embedding model', () => {
119125
beforeEach(() => {
120126
vi.clearAllMocks()
127+
resetEnvFlagsMock()
121128
mockResolveV1KnowledgeReadAccess.mockResolvedValue({ kind: 'workspace', tokens: ['pub', 'ws'] })
122129
mockAuthenticateRequest.mockResolvedValue({
123130
requestId: 'req-1',
@@ -163,6 +170,38 @@ describe('v1 knowledge search route — per-KB embedding model', () => {
163170
expect(response.status).toBe(500)
164171
})
165172

173+
it('refuses a search index while indexed organization search is dormant, before any spend', async () => {
174+
setEnvFlags({ isLiveEnterpriseSearchEnabled: true })
175+
mockCheckKnowledgeBaseAccess.mockResolvedValueOnce({
176+
hasAccess: true,
177+
knowledgeBase: { ...baseKb('kb-1', 'text-embedding-3-small'), isSearchIndex: true },
178+
})
179+
const response = await POST(
180+
createMockRequest('POST', { workspaceId: 'ws-1', knowledgeBaseIds: 'kb-1', query: 'hello' })
181+
)
182+
expect(response.status).toBe(409)
183+
expect(await response.json()).toEqual({
184+
error: 'This search index is inactive; use Sim Search.',
185+
})
186+
expect(mockGenerateSearchEmbedding).not.toHaveBeenCalled()
187+
expect(mockExecuteKnowledgeSearch).not.toHaveBeenCalled()
188+
})
189+
190+
it('searches a workspace knowledge base while indexed organization search is dormant', async () => {
191+
setEnvFlags({ isLiveEnterpriseSearchEnabled: true })
192+
mockCheckKnowledgeBaseAccess.mockResolvedValueOnce({
193+
hasAccess: true,
194+
knowledgeBase: { ...baseKb('kb-1', 'text-embedding-3-small'), isSearchIndex: false },
195+
})
196+
const response = await POST(
197+
createMockRequest('POST', { workspaceId: 'ws-1', knowledgeBaseIds: 'kb-1', query: 'hello' })
198+
)
199+
expect(response.status).toBe(200)
200+
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith(
201+
expect.objectContaining({ knowledgeBaseIds: ['kb-1'], searchIndexOnly: false })
202+
)
203+
})
204+
166205
it('retains the reader provider for ranked results and returned document metadata', async () => {
167206
const access = { kind: 'user' as const, userId: 'user-1', tokens: ['reader-token'] }
168207
const provider = {

‎apps/sim/app/api/v1/knowledge/search/route.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
import { getDocumentTagDefinitions } from '@/lib/knowledge/tags/service'
2525
import { buildUndefinedTagsError, validateTagValue } from '@/lib/knowledge/tags/utils'
2626
import type { StructuredFilter } from '@/lib/knowledge/types'
27+
import {
28+
isIndexedOrgSearchEnabled,
29+
SEARCH_INDEX_DORMANT_MESSAGE,
30+
} from '@/lib/sim-search/indexed/gate'
2731
import { checkKnowledgeBaseAccess, type KnowledgeBaseAccessResult } from '@/app/api/knowledge/utils'
2832
import { handleError, resolveV1KnowledgeReadAccess } from '@/app/api/v1/knowledge/utils'
2933
import {
@@ -121,6 +125,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
121125
)
122126
}
123127

128+
/** A search index is readable only while indexed organization search is on. */
129+
if (!isIndexedOrgSearchEnabled() && accessibleKbs.some((kb) => kb.isSearchIndex)) {
130+
return NextResponse.json({ error: SEARCH_INDEX_DORMANT_MESSAGE }, { status: 409 })
131+
}
132+
124133
let structuredFilters: StructuredFilter[] = []
125134
const tagDefsCache = new Map<string, Awaited<ReturnType<typeof getDocumentTagDefinitions>>>()
126135

‎apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/page.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { SearchParams } from 'nuqs/server'
44
import { readSearchDocumentResultSchema } from '@/lib/api/contracts/knowledge/documents'
55
import { getSession } from '@/lib/auth'
66
import { OrchestrationError } from '@/lib/core/orchestration/types'
7-
import { readSearchDocument } from '@/lib/knowledge/application/read-search-document'
7+
import { readSearchDocument } from '@/lib/sim-search/indexed'
8+
import { isIndexedOrgSearchEnabled } from '@/lib/sim-search/indexed/gate'
89
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
910
import {
1011
loadDocumentReadParams,
@@ -22,6 +23,8 @@ export default async function OrganizationDocumentPage({
2223
params,
2324
searchParams,
2425
}: OrganizationDocumentPageProps) {
26+
/** Indexed documents are served only while indexed organization search is on. */
27+
if (!isIndexedOrgSearchEnabled()) notFound()
2528
const { organizationId, knowledgeBaseId, documentId } = await params
2629
const position = await loadDocumentReadParams(searchParams, { strict: true }).catch(() =>
2730
notFound()

‎apps/sim/lib/knowledge/__integration__/excluded-member-documents.integration.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { eq, inArray } from 'drizzle-orm'
1717
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
1818

1919
const provider = vi.hoisted(() => ({ list: vi.fn(), get: vi.fn(), changes: vi.fn() }))
20+
/** This suite covers indexed organization search, which is dormant unless Live Search is off. */
21+
vi.mock('@/lib/core/config/env-flags', async (importOriginal) => ({
22+
...(await importOriginal<typeof import('@/lib/core/config/env-flags')>()),
23+
isLiveEnterpriseSearchEnabled: false,
24+
}))
2025
vi.mock('@/connectors/registry.server', () => ({
2126
CONNECTOR_REGISTRY: {
2227
google_drive: {

‎apps/sim/lib/knowledge/__integration__/github-member.integration.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import { generateId } from '@sim/utils/id'
3030
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
3131
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3232

33+
/** This suite covers indexed organization search, which is dormant unless Live Search is off. */
34+
vi.mock('@/lib/core/config/env-flags', async (importOriginal) => ({
35+
...(await importOriginal<typeof import('@/lib/core/config/env-flags')>()),
36+
isLiveEnterpriseSearchEnabled: false,
37+
}))
3338
vi.mock('@/lib/embeddings', async () => ({
3439
...(await import('@/lib/embeddings/client')),
3540
assertKnowledgeEmbeddingCapacity: async () => {},
@@ -83,7 +88,6 @@ import {
8388
listKnowledgeConnectorDocuments,
8489
} from '@/lib/knowledge/application/connectors'
8590
import { readKnowledgeDocument } from '@/lib/knowledge/application/documents'
86-
import { readIndexedKnowledgeDocument } from '@/lib/knowledge/application/read-indexed-document'
8791
import { searchKnowledge } from '@/lib/knowledge/application/search'
8892
import { readSearchSourceOverview } from '@/lib/knowledge/application/search-source-overview'
8993
import { listSearchSources } from '@/lib/knowledge/application/search-sources'
@@ -95,6 +99,7 @@ import {
9599
} from '@/lib/knowledge/connectors/sync-limits'
96100
import { getDocuments } from '@/lib/knowledge/documents/service'
97101
import { getTagUsageStats } from '@/lib/knowledge/tags/service'
102+
import { readIndexedKnowledgeDocument } from '@/lib/sim-search/indexed/documents/read-indexed-document'
98103
import { deleteFile } from '@/lib/uploads/core/storage-service'
99104
import { downloadFileFromUrl } from '@/lib/uploads/utils/file-utils.server'
100105
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'

‎apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks'
8686
import { updateKnowledgeConnectorAccess } from '@/lib/knowledge/application/connector-access'
8787
import { updateKnowledgeConnector } from '@/lib/knowledge/application/connectors'
8888
import { readKnowledgeDocument } from '@/lib/knowledge/application/documents'
89-
import { readSearchDocument } from '@/lib/knowledge/application/read-search-document'
9089
import { searchKnowledge } from '@/lib/knowledge/application/search'
9190
import { executeSync } from '@/lib/knowledge/connectors/sync-engine'
91+
import { readSearchDocument } from '@/lib/sim-search/indexed/documents/read-search-document'
9292
import * as storage from '@/lib/uploads/core/storage-service'
9393
import { downloadFileFromUrl } from '@/lib/uploads/utils/file-utils.server'
9494
import { PATCH as updateConnectorRoute } from '@/app/api/knowledge/[id]/connectors/[connectorId]/route'

‎apps/sim/lib/knowledge/__integration__/gmail-member.integration.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import { eq, inArray } from 'drizzle-orm'
2424
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
2525

2626
const counters = vi.hoisted(() => ({ embeddedTexts: 0 }))
27+
/** This suite covers indexed organization search, which is dormant unless Live Search is off. */
28+
vi.mock('@/lib/core/config/env-flags', async (importOriginal) => ({
29+
...(await importOriginal<typeof import('@/lib/core/config/env-flags')>()),
30+
isLiveEnterpriseSearchEnabled: false,
31+
}))
2732
vi.mock('@/lib/embeddings', async () => ({
2833
...(await import('@/lib/embeddings/client')),
2934
assertKnowledgeEmbeddingCapacity: async () => {},

‎apps/sim/lib/knowledge/__integration__/google-calendar-member.integration.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import { generateId } from '@sim/utils/id'
2424
import { and, eq, inArray, isNull } from 'drizzle-orm'
2525
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
2626

27+
/** This suite covers indexed organization search, which is dormant unless Live Search is off. */
28+
vi.mock('@/lib/core/config/env-flags', async (importOriginal) => ({
29+
...(await importOriginal<typeof import('@/lib/core/config/env-flags')>()),
30+
isLiveEnterpriseSearchEnabled: false,
31+
}))
2732
vi.mock('@/lib/embeddings', async () => ({
2833
...(await import('@/lib/embeddings/client')),
2934
assertKnowledgeEmbeddingCapacity: async () => {},

0 commit comments

Comments
 (0)