Skip to content
bitzorcas
中EN

Guide

Documents Reads, Search, Cache, Files, and Cloud Drives

QueryShape reads, in-memory search, caching, index events, the simplified file service, Files/Search boundaries, and the current cloud-drive adapter.

Last updated

The authoritative read path uses unified aggregates and QueryShape. Search, cache, file, and cloud-drive services are at different maturity levels. Before relying on an interface, verify who calls it, whether it has an HTTP endpoint, and whether it persists anything.

1. Read path

GET detail/list/tree/version

Query handler

ICurrentUser.User.TenantId

IDocumentsReadModelStore

QueryShape list projection

full aggregate detail

Lists use dedicated projections and cap PageSize at 100. Details load complete aggregates, including embedded JSON. Main-resource reads use User.TenantId, not EffectiveTenantId, and apply neither member nor ACL predicates.

QueryRead behaviorBoundary
Document detailTenant + Id, full aggregateincludeVersions is ignored
Document listTenant plus optional KB/category/status/type/keywordKeyword covers Title/Description only
Knowledge-base treeLoad all tenant KBs, recurse in memoryNo access filter or cycle detection
Category treeLoad Tenant + KB categories, recurse in ApplicationNo cycle or access guard
Whiteboard detail/listTenant predicateDetail includes full Data; members ignored
Collaboration sessionId or DocumentId + ActiveNo tenant predicate

2. In-memory search algorithm

DocumentSearchService is not an external full-text index. On a cache miss it reads the first page of at most 200 summaries, filters and scores them in process, then paginates that subset.

ReadModelStoreDocumentSearchServiceIAppCacheSearch query handlerReadModelStoreDocumentSearchServiceIAppCacheSearch query handlertenant + filters + keyword + pageGetOrSet(key, 2 minutes)cache-miss factorypage 1, size 200, keyword=nullsummariestags + title/description scoresort and requested page

Title contains adds 0.7, title prefix adds 0.2, short-title density adds up to roughly 0.09, and description contains adds 0.3, capped at 1.0. Content is never searched. Documents after candidate 200 are invisible, and TotalCount means “matches in this subset,” not the tenant-wide result count.

3. Search has no HTTP route

Search and suggestion messages implement IAuthorizedRequest but lack [GenerateEndpoint]. The following is an in-process Mediator call, not evidence of /api/v1/documents/search:

In-process search
// This message is available through Mediator, not a generated HTTP route.
var result = await mediator.Send(
new SearchDocumentsQuery(
Keyword: "refund",
KnowledgeBaseId: "kb-support",
CategoryId: null,
ContentType: ContentType.Markdown,
Status: DocumentStatus.Published,
Tags: ["support"],
PageIndex: 1,
PageSize: 20),
cancellationToken);
// Preserve dependency failure instead of presenting it as a real empty result.
if (result.IsFailure)
return Result.Failure<SearchView>(result.Error);
// Results cover at most the first 200 candidates returned by the read model.
return SearchView.From(result.Value!);

An HTTP product route needs explicit endpoint generation, resource visibility, input limits, stable ordering, real totals, and contract tests.

4. Highlighting and XSS

The highlighter inserts <mark> into raw Title/Description without HTML encoding. If the UI renders the result as raw HTML, an original title such as <img src=x onerror=alert(1)>refund remains executable.

Prefer structured fragments or ranges:

Render structured highlights safely
type HighlightPart = { text: string; matched: boolean };
export function renderTitle(parts: HighlightPart[]): HTMLElement {
const container = document.createElement("span");
// Structured ranges choose element boundaries; source values remain text only.
for (const part of parts) {
const node = part.matched
? document.createElement("mark")
: document.createElement("span");
// textContent encodes untrusted source text.
node.textContent = part.text;
container.append(node);
}
return container;
}

Until the response contract changes, clients must not feed the highlight string directly to innerHTML.

5. Cache and index-event gaps

Search cache TTL is two minutes. The key contains raw tenant, keyword, filters, sorted tags, and page values, exposing search terms to cache diagnostics and allowing oversized input to expand keys.

When the read model fails, ExecuteSearchAsync returns an empty successful result. The outer GetOrSetAsync can cache that result despite the comment saying failures are not cached, creating a two-minute false negative.

docs.document.* integration event

DocumentIndexEventHandler

IndexDocument / DeleteIndex

log only, Success

Five event contracts and subscribers exist, but no producer for those integration events was found. The aggregate raises domain events only for Publish and Archive, and no bridge to the five integration contracts was found. The subscriber suppresses non-cancellation exceptions to prevent retry storms.

DocumentCacheService provides a tenant-scoped ten-minute detail cache, but no query/command handler calls it. It is registered infrastructure, not active read caching.

6. The document file service is not Files

MethodActual behaviorNot provided
UploadAsyncValidates basic fields, logs a warning, returns stream lengthNo byte persistence or StorageKey update
DownloadAsyncReturns UTF-8 bytes of Document.ContentNo object-store read or conversion
PDF/Word branchLabels text bytes with PDF/DOCX MIME and extensionNot a valid PDF or DOCX file
Correct binary attachment boundary
// Store and finalize binary bytes through Files first.
var asset = await files.UploadAndFinalizeAsync(source, cancellationToken);
// Documents owns only the business relationship to a stable FileId.
var attachment = DocumentAttachment.Create(
documentId,
asset.FileId,
displayName: "refund-evidence.pdf");
// Download authorization and temporary URLs remain in Files.
return await files.GetDownloadAccessAsync(attachment.FileId, cancellationToken);

Markdown-to-PDF requires an explicit rendering/export use case that generates real bytes and stores them through Files.

7. Dormant storage and view fields

Document.SetStorage and IncrementViewCount exist. Source search finds the aggregate definitions and unit coverage for view count, but no application handler calls them. StorageKey/FileSize may remain defaults, details do not increment views, and ViewCount must not drive billing, recommendation, or SLA reporting.

A separate, deduplicated analytics event path avoids write contention on the document aggregate.

8. Cloud-drive port

ICloudSyncPort exposes Upload, ListFiles, Search, and ProviderName. CloudDriveAdapter maps a vendor ICloudDriveProvider into neutral Id/Name/Size/ContentType/DownloadUrl/ModifiedTime records.

The adapter has no registration attribute, no observed host registration for ICloudSyncPort, and no Documents caller. A real sync product still needs credential ownership, remote-path mapping, cursors, conflict/deletion policies, idempotency, retry/rate limits, reconciliation, URL security, scheduling, and operator status.

9. Production indexing shape

detail remains authoritative

Document transaction

transactional outbox

idempotent indexer

shared Search engine

authorized search API

Keep the in-memory service as a development/small-data fallback. A production index needs stable EventId idempotency, tenant partitions, ACL visibility, retryable failures, and an index-rebuild operation.

10. Required tests

  • recall and TotalCount with more than 200 documents;
  • dependency failures must not become cached empty successes;
  • HTML, Unicode, long terms, and cache-key limits;
  • index visibility after member/ACL changes;
  • a true lightweight projection for includeVersions=false;
  • PDF/Word responses cannot be mislabeled text;
  • cloud-drive duplicate, throttled, and partial-failure paths;
  • tenant predicates on every collaboration-session query.

Back to Documents

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%