Platform Guide
Enterprise Search
One search box over every source: documents, datasets, apps, agents, pipelines, KG facts, and messages. Results from three legs are fused and filtered so you only see what you can access, with a citation for what you can see.
Not a second ingest system. Enterprise Search is a query and fusion service over the durable Postgres catalog index (lace.search_catalog_entity), the document leg, and the KG leg. It does not re-crawl your sources.
How it works — three legs, one ranking
POST /v1/enterprise-search ──▶ EnterpriseSearchService.search()
├── catalog leg ── PostgresCatalogSearchBackend (tsvector + trgm) ──┐
├── document leg ── retrieve_segments (pgvector hybrid) ─────────────┤── RRF fuse (k=60) ── ACL finalize ── paginate
└── kg leg ─────── chat_agent_service_runner / kg.facts (lexical) ──┘
│ │
planner (lexical-only today, vector parked D3)
# service.py — three legs in parallel, RRF fuse, ACL finalize
results = await asyncio.gather(
catalog_search(req), # Postgres tsvector + pg_trgm over lace.search_catalog_entity
document_search(req), # pgvector hybrid (state.retrieve_segments)
kg_search(req), # kg.facts lexical (plainto_tsquery, AND semantics)
)
fused = reciprocal_rank_fuse(results, k=60)
visible = [r for r in fused if envelope_visible_to_principals(r, principals)]
| Leg | Source | Backend | What it catches |
|---|---|---|---|
catalog | lace.search_catalog_entity | PostgresCatalogSearchBackend (default) | Every LACE entity with browse and facets |
document | pgvector segments | state.retrieve_segments | Paraphrase and semantic recall |
kg | KG entities + evidence | schema_extraction store | Facts with citations |
Hybrid retrieval
A lexical leg and a vector leg run against the same corpus and their rankings are combined with reciprocal-rank fusion (RRF, src/lace/domain/enterprise_search/rrf.py, k=60). Lexical catches the exact identifier or clause reference that embeddings blur. Vector catches the paraphrase that keyword search misses. Fusing them avoids having to choose which failure mode to live with.
| Leg | Query transform | Index | Recall |
|---|---|---|---|
| Lexical | plainto_tsquery / to_tsvector + pg_trgm | tsvector GIN + trigram GIN on lace.search_catalog_entity | Exact identifiers, BM25-like ranking |
| Vector | Embedding of query (same encoder as ingest) | pgvector IVFFLAT / HNSW over segment embeddings | Semantic recall |
The vector leg is currently parked and the planner is lexical-only (src/lace/domain/enterprise_search/planner.py, vector re-enable is D3). The fusion point and RRF scorer already exist, so the vector leg can reattach without changing callers.
Permissions are not post-filtering
Retrieval resolves your principals from auth against each source's permission snapshots taken at sync time (datasets). The platform enforces ACL in SQL as a prefilter, so unauthorized rows never leave Postgres, and again in the Python finalize. Facet counts are recomputed over the visible set only so they never leak a restricted document's existence.
Code: src/lace/domain/enterprise_search/acl.py + catalog.py.
GPU rerank
After fusion, the sidecar at services/reranker (GPU, cross-encoder) may rerank results. The reranker sees the original query plus each candidate's text and re-scores with a cross-attention model. Opt in per request (rerank: true) or per tenant default. When the reranker is saturated or disabled, the RRF ranking is the final ranking.
Stable identity
Every retrieved result has a stable triple — document_id, block_id, span [start, end] — that survives re-ingest. That triple is what citations point to and what the knowledge graph's evidence checks gate on. See src/lace/domain/document_evidence and src/lace/domain/ingest.
Query shape
curl -X POST https://api.laceplatform.com/v1/enterprise-search \
-H "Authorization: Bearer $LACE_API_KEY" \
-d '{
"query": "Q3 warranty reserves",
"limit": 10,
"facets": ["entity_type","dataset"],
"rerank": true
}'| Field | What it does |
|---|---|
query | Lexical query (today, plainto_tsquery; vector leg parked, query planner is lexical-only) |
limit / cursor | Paginated; stable over index updates |
facets | Request facet counts (entity type, dataset, connector, etc.) |
rerank | When true, results are reranked on GPU via services/reranker |
and_match | Boosts hits that matched all query terms (lexical AND semantics) over OR-recall hits |
The durable catalog index
lace.search_catalog_entity is a Postgres table (tsvector + pg_trgm + GIN indexes) fed by CatalogIndexer: project → diff → sync (bootstrap + reconcile) plus event-driven DocumentStubSyncWorker on ingest success and dataset CRUD. The write store lives at src/lace/api/stores/repositories/search_catalog.py::PostgresCatalogStore.
Versioning is single monotonic space (versioning.py) so external_gte gating is coherent and bootstrap is idempotent (second run = drift 0).
No per-query live projection on the hot path. The durable index is O(index). The live-projection fallback surfaces a catalog_index_fallback warning when it fires.
Citations
Every document and KG result carries its source span — document, block, and char offsets — so the answer can point to the paragraph that supports it. The RAG path uses the same legs; enterprise search is the browse and answer surface, RAG is the grounded generation surface.
RAG — grounded generation
RAG is the same retrieval stack plus a generation pass that is required to cite. The prompt is assembled from the top-N fused, ACL-filtered, reranked spans. The model is instructed to only assert what a span supports. The response carries the spans back as citations. The retrieval and generation spans share trace ids (see observability) so you can click a citation from the answer back to the document block.
from lace.domain.rag import rag_answer
answer = await rag_answer(
query="What did Q3 say about warranty reserves?",
dataset_ids=["0141b..."],
tenant_id="acme",
principals=["user:4821"],
)
# → { text, citations: [{ document_id, block_id, span: [start, end], score }] }
Ops
python -m lace.domain.enterprise_search.ops bootstrap— backfill the catalog.python -m lace.domain.enterprise_search.ops reconcile— drift detection and sync.- See
src/lace/domain/enterprise_search/README.mdanddocs/agent/enterprise-search.mdfor the full design.
Next: datasets or the marketing page.