Skip to content

perf(search): parallelize cross-source searches and cache query embeddings#119

Merged
jeffgreendesign merged 7 commits into
mainfrom
claude/80-20-optimizations-analysis-il5rlu
Jul 11, 2026
Merged

perf(search): parallelize cross-source searches and cache query embeddings#119
jeffgreendesign merged 7 commits into
mainfrom
claude/80-20-optimizations-analysis-il5rlu

Conversation

@jeffgreendesign

Copy link
Copy Markdown
Owner

Two low-risk runtime wins on the search hot path, plus a CI cleanup:

  • unifiedSearch ran document -> memory -> conversation searches serially even
    though all three depend only on the query embedding. Fuse them into one
    Promise.all so their DB round-trips overlap. Output shape and scoring are
    unchanged.
  • Add a bounded (LRU + TTL) query-embedding cache so repeated queries skip the
    external embedding provider round-trip. Keyed by provider+model so a config
    change can't return a wrong-dimension vector; only successful embeddings are
    cached. Covers all three sources since they share the query embedding.
  • Add concurrency groups with cancel-in-progress to the CI, CodeQL, and
    docs-deploy workflows so superseded pushes stop wasting runners.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6

claude added 6 commits July 10, 2026 12:20
…dings

Two low-risk runtime wins on the search hot path, plus a CI cleanup:

- unifiedSearch ran document -> memory -> conversation searches serially even
  though all three depend only on the query embedding. Fuse them into one
  Promise.all so their DB round-trips overlap. Output shape and scoring are
  unchanged.
- Add a bounded (LRU + TTL) query-embedding cache so repeated queries skip the
  external embedding provider round-trip. Keyed by provider+model so a config
  change can't return a wrong-dimension vector; only successful embeddings are
  cached. Covers all three sources since they share the query embedding.
- Add concurrency groups with cancel-in-progress to the CI, CodeQL, and
  docs-deploy workflows so superseded pushes stop wasting runners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
…tor 0.8 iterative scans

Fixes a correctness bug: filtering tags/sourceType/contentType in JS after
fetching only the top limit*3 RRF chunks could return 0 results even when
matching documents exist, because the matching chunks never entered the fetch
window.

- hybrid_search() now takes filter_source_type / filter_content_type /
  filter_tags and applies them inside BOTH the full_text and semantic CTEs
  (before their limit), so selective filters are never starved. Replicated
  across all four dimension variants (1536/3072/1024/768), which stay identical
  modulo vector(N). Old 6-arg signature is dropped first to avoid an ambiguous
  overload.
- Add a GIN index on documents.metadata (jsonb_path_ops) to back the tag /
  content_type predicates.
- Enable pgvector 0.8 hnsw.iterative_scan per connection (exception-guarded so
  older pgvector still connects), so the filtered HNSW scan keeps going until
  the LIMIT is met. Also benefits memory search's existing entity_types filter.
- unifiedSearch forwards the categorical filters to SQL and drops the JS
  post-filters for them; minScore stays in JS (it also gates weighted
  cross-source scores) and only it now widens the fetch.
- Add a dated idempotent migration and note pgvector >= 0.8 in the install docs.
- New tests for the DB param mapping and the service-level filter pushdown.

Validated the SQL end-to-end against a real Postgres 16 (pgvector bits stubbed):
filter predicates, tag AND-semantics, the 9-arg signature, and the GIN index
all behave as intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
…search

Finishes the two remaining Tier-2 runtime items.

#5 HNSW ef_search:
- Set hnsw.ef_search per connection (default 100, env HNSW_EF_SEARCH), alongside
  the existing iterative_scan GUC, in the same exception-guarded DO block so
  older pgvector still connects. Read via a local envInt() helper to keep the DB
  layer decoupled from config. Raises recall over pgvector's built-in default of
  40. Validated against a real Postgres 16: the DO block swallows the unknown-GUC
  error on plain PG and leaves the connection usable.

#6 ingest throughput:
- Embedding batches within one generateEmbeddings call now run with bounded,
  order-preserving concurrency via a shared embedBatchesConcurrently() helper
  (pLimit + Promise.all + flat). Applied to all three providers; OpenAI keeps its
  index re-sort. Biggest win for semantic chunking and large single embeds.
- embedAndStoreChunks runs its 128-chunk embed+insert windows with bounded
  concurrency, overlapping embed and insert for large documents. Each chunk keeps
  its own chunkIndex so insert order is irrelevant; peak memory stays bounded to
  ~concurrency windows.
- Two new int knobs (EMBEDDING_BATCH_CONCURRENCY, EMBED_WINDOW_CONCURRENCY, both
  default 2); documented in .env.example and docs/ENV.md.

Tests: unit-test the concurrency helper (order preserved under reverse-completion,
respects the bound) and embed-store (correct chunkIndex<->embedding pairing across
concurrent windows, length-mismatch guard still throws). Updated the
upload-processor config mock for the new keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
Bump root zod ^3.23.0 -> ^4.4.3. The lockfile now dedups to a single
zod@4.4.3 instance shared by @modelcontextprotocol/sdk (1.29, declares
zod ^3.25 || ^4.0), @modelcontextprotocol/ext-apps, @anthropic-ai/sdk, and
zod-to-json-schema — so the tool schemas (passed as raw zod objects to
registerTool) and the SDK share one instance, avoiding the dual-instance
hazard. The openai@4.104 peer warning for zod ^3 is inert; we only use OpenAI
for embeddings, not its zod-based helpers.

Modernize the only two deprecated-in-v4 spots:
- z.string().url() -> z.url() (5 sites in config + url tool)
- result.error.format() -> z.treeifyError() in config validation logging

Every other zod usage was already v4-correct (.issues accessors, two-arg
z.record, positional custom messages, .default().transform() chains).

Verified: typecheck, 444 tests, esbuild build, createMcpServer tool
registration, and a live MCP tools/list over HTTP (all 25 tools serialize
their schemas to JSON Schema with no error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
pdf-parse 1.1.x is long-unmaintained and runs a debug self-test on import.
Swap it for unpdf (maintained, pdfjs-based) on the server + CLI extraction
paths. API surface is tiny — buffer in, text out:

- src/services/processor/handlers/pdf.ts and the takeout CLI converter now call
  unpdf's extractText(new Uint8Array(buffer), { mergePages: true }).
- Drop pdf-parse from the root package.json (add unpdf); delete the obsolete
  ambient src/types/pdf-parse.d.ts (unpdf ships its own types).
- Update the registry test mock to unpdf.

The desktop app intentionally keeps pdf-parse for now — its Electron main
bundle (esbuild externals + electron-builder packaging) can't be verified in
this environment.

Verified: typecheck, 444 tests (incl. the registry PDF path), esbuild build,
and a real end-to-end extraction (unpdf pulled "Hello Textrawl PDF" out of a
generated one-page PDF).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
Bump @biomejs/biome 1.9.4 -> ^2.0.0 (resolves 2.5.3) in root, website, and
dashboard, kept in lockstep. Ran `biome migrate` to convert biome.json to the v2
shape: $schema bump, organizeImports -> assist.actions.source.organizeImports,
files.ignore -> files.includes (negated globs), overrides[].include -> includes.

Biome 2's expanded recommended set surfaced new findings; reconciled to keep CI
green while preserving the project's existing lenient posture:
- Real fixes: two forEach callbacks that implicitly returned a value
  (useIterableCallbackReturn) in the html + takeout converters and the desktop
  file-router now use block bodies.
- Excluded non-source assets/fixtures from linting (public/**, __tests__/fixtures).
- Softened brand-new v2 rules the project never enforced to warn/off, matching how
  it already softens noExplicitAny/noForEach/noNonNullAssertion: noArrayIndexKey,
  useButtonType, noStaticElementInteractions, noAmbiguousAnchorText,
  noSvgWithoutTitle (warn); noImportantStyles (off — !important is intentional in
  the docs/dashboard CSS).
- Applied v2's safe autofixes: dead-import/var removal, parseInt radix, optional
  chaining, and the new import-sort order across the repo (mechanical, no semantic
  change — keeps the tree consistent with `biome check --write`).

Remaining 24 warnings are minor pre-existing suggestions (unused params, array
index keys) newly surfaced by v2; non-blocking (biome check exits 0).

Verified: typecheck, 444 tests, esbuild build, and `biome check` exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Building Building Preview, Comment Jul 11, 2026 4:28pm
textrawl Ready Ready Preview, Comment Jul 11, 2026 4:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b39ab316-fe78-4449-9b19-6abd9113ef82

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This change adds filtered hybrid-search support with metadata indexes and pgvector tuning, bounded concurrency for embedding and chunk storage, and cached query embeddings. PDF extraction moves from pdf-parse to unpdf, while Zod and Biome configurations are updated. CI, CodeQL, and website deployment workflows now cancel superseded runs. Numerous imports, formatting patterns, environment documentation, and unused symbols are also updated.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UnifiedSearch
  participant QueryEmbeddingCache
  participant HybridSearch
  participant PostgreSQL

  Client->>UnifiedSearch: Submit search query and filters
  UnifiedSearch->>QueryEmbeddingCache: Get query embedding
  QueryEmbeddingCache-->>UnifiedSearch: Return cached or generated embedding
  UnifiedSearch->>HybridSearch: Pass embedding and metadata filters
  HybridSearch->>PostgreSQL: Execute filtered hybrid_search query
  PostgreSQL-->>HybridSearch: Return ranked results
  HybridSearch-->>UnifiedSearch: Return document results
  UnifiedSearch-->>Client: Return unified search response
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the changes but does not use the required template sections or include the checklist and related issues. Rewrite it using the repo template with Summary, Changes, Type of Change, Checklist, and Related Issues sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main performance changes: concurrent searches and query-embedding caching.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/80-20-optimizations-analysis-il5rlu

Comment @coderabbitai help to get the list of available commands.

Comment thread scripts/cli/converters/mbox.ts Fixed
Comment thread scripts/cli/converters/mbox.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/cli/converters/takeout.ts`:
- Around line 30-34: Update the unhandledRejection handler to route its warning
through the existing logger by replacing the direct console.error call with
logger.error, while preserving the current message and continue-on-rejection
behavior.

In `@scripts/migrations/2026-07-10-hybrid-search-filters.sql`:
- Line 22: Update the documents_metadata_gin_idx creation statement to use
CREATE INDEX CONCURRENTLY, and ensure this migration runs outside a transaction
because PostgreSQL does not allow concurrent index creation within one.

In `@scripts/setup-db.sql`:
- Around line 90-92: Update the JSONB filter predicates in both CTEs to use
top-level metadata containment with d.metadata @> jsonb_build_object(...),
replacing the content_type extraction equality and nested tags containment
checks while preserving their existing null-filter behavior.

In `@src/services/__tests__/embed-store.test.ts`:
- Around line 49-55: Add an assertion to the embedding-count mismatch test for
embedAndStoreChunks that verifies createChunks was not called after the
rejection. Keep the existing rejection assertion and ensure the mock is
available for checking this promised no-write behavior.

In `@src/services/embed-store.ts`:
- Around line 35-59: The chunk-writing flow around generateEmbeddings,
createChunks, and Promise.all must prevent partial persistence when any window
fails. Add cancellation or compensating cleanup for already-written windows,
ensuring no createChunks operation remains able to commit after the first
failure and the overall upload leaves no partial chunk rows.

In `@src/utils/config.ts`:
- Around line 52-63: Update EMBEDDING_BATCH_CONCURRENCY and
EMBED_WINDOW_CONCURRENCY to use z.coerce.number().int().min(1) instead of string
parsing with parseInt, ensuring malformed and fractional environment values are
rejected. Preserve their existing default of 2 and do not add a maximum
constraint unless an established concurrency ceiling exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0c3d01b-0140-49f9-8c9e-4e2683553554

📥 Commits

Reviewing files that changed from the base of the PR and between a09a784 and 98f0ed5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (93)
  • .env.example
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/deploy-website.yml
  • biome.json
  • dashboard/app/insights/page.tsx
  • dashboard/app/knowledge/page.tsx
  • dashboard/app/memory/page.tsx
  • dashboard/app/page.tsx
  • dashboard/app/upload/page.tsx
  • dashboard/lib/__tests__/api.test.ts
  • dashboard/lib/api.ts
  • dashboard/lib/queries.ts
  • dashboard/package.json
  • desktop/src/main/index.ts
  • desktop/src/main/services/document-processor.ts
  • desktop/src/main/services/file-router.ts
  • desktop/src/main/services/project-manager.ts
  • desktop/src/main/utils/run-cli-script.ts
  • docs/ENV.md
  • docs/getting-started/installation.mdx
  • package.json
  • scripts/cli/converters/eml.ts
  • scripts/cli/converters/facebook.ts
  • scripts/cli/converters/html.ts
  • scripts/cli/converters/instagram.ts
  • scripts/cli/converters/mbox.ts
  • scripts/cli/converters/reddit.ts
  • scripts/cli/converters/spotify.ts
  • scripts/cli/converters/takeout.ts
  • scripts/cli/lib/analyze.ts
  • scripts/cli/lib/normalizer.ts
  • scripts/cli/lib/security.ts
  • scripts/cli/lib/splitter.ts
  • scripts/cli/scan.ts
  • scripts/cli/split.ts
  • scripts/cli/upload.ts
  • scripts/migrations/2026-07-10-hybrid-search-filters.sql
  • scripts/setup-db-google.sql
  • scripts/setup-db-ollama-v2.sql
  • scripts/setup-db-ollama.sql
  • scripts/setup-db.sql
  • scripts/ui/server.ts
  • src/api/__tests__/upload-process.test.ts
  • src/api/insight-routes.ts
  • src/api/middleware/rateLimit.ts
  • src/api/oauth/jwt.ts
  • src/api/oauth/routes.ts
  • src/api/routes.ts
  • src/api/upload-process.ts
  • src/api/upload-sessions.ts
  • src/db/__tests__/search.test.ts
  • src/db/memory-relations.ts
  • src/db/memory-search.ts
  • src/db/pg-client.ts
  • src/db/search.ts
  • src/db/stats.ts
  • src/index.ts
  • src/server.ts
  • src/services/__tests__/embed-store.test.ts
  • src/services/__tests__/embeddings.concurrency.test.ts
  • src/services/__tests__/search.test.ts
  • src/services/__tests__/upload-processor.test.ts
  • src/services/embed-store.ts
  • src/services/embeddings.ts
  • src/services/memory-extraction.ts
  • src/services/pg-analyze/formatter.ts
  • src/services/pg-analyze/history.ts
  • src/services/processor/__tests__/archive-zip.test.ts
  • src/services/processor/__tests__/registry.test.ts
  • src/services/processor/handlers/pdf.ts
  • src/services/search.ts
  • src/services/upload-processor.ts
  • src/tools/index.ts
  • src/tools/insights.ts
  • src/tools/memory.ts
  • src/tools/pg-analyze.ts
  • src/tools/url.ts
  • src/types/index.ts
  • src/types/pdf-parse.d.ts
  • src/ui/index.ts
  • src/utils/__tests__/query-embedding-cache.test.ts
  • src/utils/__tests__/source-span.test.ts
  • src/utils/config.ts
  • src/utils/query-embedding-cache.ts
  • website/app/(home)/layout.tsx
  • website/app/(home)/page.tsx
  • website/app/api/search/route.ts
  • website/app/docs/[[...slug]]/page.tsx
  • website/app/docs/layout.tsx
  • website/app/layout.config.tsx
  • website/components/landing/hero.tsx
  • website/package.json
💤 Files with no reviewable changes (3)
  • src/types/pdf-parse.d.ts
  • src/db/stats.ts
  • src/index.ts

Comment thread scripts/cli/converters/takeout.ts
Comment thread scripts/migrations/2026-07-10-hybrid-search-filters.sql Outdated
Comment thread scripts/setup-db.sql Outdated
Comment thread src/services/__tests__/embed-store.test.ts
Comment thread src/services/embed-store.ts
Comment thread src/utils/config.ts
- hybrid_search JSONB filters now use top-level containment so the new
  documents_metadata_gin_idx is actually used: metadata @> jsonb_build_object(
  'content_type'|'tags', ...) instead of metadata->>'content_type' = ... and
  metadata->'tags' @> ... (the latter can't use a GIN index on `metadata`).
  Validated against Postgres 16: identical result sets, and the new form plans a
  Bitmap Index Scan (incl. via a parameterized statement) where the old form
  seq-scans. Applied to all four setup-db*.sql variants + the migration.
- Migration builds the GIN index with CREATE INDEX CONCURRENTLY (documented as
  autocommit-only) so it doesn't block writes on a live documents table.
- mbox converter: drop the dead _prevLineEmpty assignments (CodeQL useless-assignment).
- takeout converter: route the unhandledRejection warning through logger.error
  instead of console.error, per the logging guideline.
- embed-store test: assert createChunks is not called on an embedding-count
  mismatch (locks in the no-partial-write guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PG8KhXDuHgAqNEcvLVXxi6

Copy link
Copy Markdown
Owner Author

Addressed the review in 13334f9:

  • JSONB filter index usage — switched both CTEs to metadata @> jsonb_build_object(...) across all four setup-db*.sql + the migration. Validated on Postgres 16: identical result sets to the old predicates, and the new form plans a Bitmap Index Scan on documents_metadata_gin_idx (incl. via a parameterized statement) where metadata->'tags' @> ... seq-scans.
  • Migration now uses CREATE INDEX CONCURRENTLY (documented as autocommit-only).
  • mbox dead _prevLineEmpty removed; takeout rejection handler routes through logger.error; embed-store test asserts createChunks isn't called on a count mismatch.
  • Left the window-atomicity and z.coerce suggestions with rationale in-thread.

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@jeffgreendesign Thanks for the detailed rundown — the switch to metadata @> jsonb_build_object(...) for GIN-friendly plans, the CREATE INDEX CONCURRENTLY migration, and the cleanup items all sound solid. I'll kick off a fresh review to verify everything end-to-end.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jeffgreendesign
jeffgreendesign merged commit faead7e into main Jul 11, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants