521 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			521 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 🏗️ ARCHITECTURE CIBLE & SCHÉMAS
 | 
						|
 | 
						|
## 1. Diagramme Architecture Globale
 | 
						|
 | 
						|
```
 | 
						|
┌─────────────────────────────────────────────────────────────────┐
 | 
						|
│                         FRONTEND (Angular 20)                   │
 | 
						|
├─────────────────────────────────────────────────────────────────┤
 | 
						|
│                                                                 │
 | 
						|
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐           │
 | 
						|
│  │   UI Layer   │  │ Main Thread  │  │ Web Workers  │           │
 | 
						|
│  │              │  │              │  │              │           │
 | 
						|
│  │ • Components │  │ • Services   │  │ • Markdown   │           │
 | 
						|
│  │ • OnPush CD  │  │ • Signals    │  │   Parser     │           │
 | 
						|
│  │ • Virtual    │  │ • HTTP       │  │ • Search     │           │
 | 
						|
│  │   Scroll     │  │ • State Mgmt │  │   Index      │           │
 | 
						|
│  │              │  │              │  │ • Graph      │           │
 | 
						|
│  └──────────────┘  └──────────────┘  │   Layout     │           │
 | 
						|
│         │                  │          └──────────────┘          │
 | 
						|
│         └──────────────────┴─────────────────┘                  │
 | 
						|
│                            │                                    │
 | 
						|
└────────────────────────────┼────────────────────────────────────┘
 | 
						|
                             │ HTTP/JSON
 | 
						|
                             ▼
 | 
						|
┌─────────────────────────────────────────────────────────────────┐
 | 
						|
│                    BACKEND (Node.js Express)                    │
 | 
						|
├─────────────────────────────────────────────────────────────────┤
 | 
						|
│                                                                 │
 | 
						|
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐           │
 | 
						|
│  │  API Routes  │  │   Services   │  │   Watchers   │           │
 | 
						|
│  │              │  │              │  │              │           │
 | 
						|
│  │ • /api/vault │  │ • VaultSvc   │  │ • Chokidar   │           │
 | 
						|
│  │ • /api/search│  │ • SearchSvc  │  │ • SSE Events │           │
 | 
						|
│  │ • /api/log   │  │ • LogSvc     │  │              │           │
 | 
						|
│  │ • /api/health│  │ • CacheSvc   │  │              │           │
 | 
						|
│  └──────────────┘  └──────────────┘  └──────────────┘           │
 | 
						|
│         │                  │                                    │
 | 
						|
│         └──────────────────┴─────────────────┐                  │
 | 
						|
│                                               ▼                 │
 | 
						|
│                                    ┌──────────────────┐         │
 | 
						|
│                                    │   Meilisearch    │         │
 | 
						|
│                                    │   (Search Engine)│         │
 | 
						|
│                                    │                  │         │
 | 
						|
│                                    │ • Index vault_{} │         │
 | 
						|
│                                    │ • Typo-tolerance │         │
 | 
						|
│                                    │ • Facets         │         │
 | 
						|
│                                    │ • Highlights     │         │
 | 
						|
│                                    └──────────────────┘         │
 | 
						|
└─────────────────────────────────────────────────────────────────┘
 | 
						|
                             │
 | 
						|
                             ▼
 | 
						|
              ┌──────────────────────────┐
 | 
						|
              │   Filesystem / Volumes   │
 | 
						|
              │                          │
 | 
						|
              │  • /app/vault (Obsidian) │
 | 
						|
              │  • /app/db (logs, cache) │
 | 
						|
              │  • /app/tmp              │
 | 
						|
              └──────────────────────────┘
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 2. Schéma d'Index Meilisearch
 | 
						|
 | 
						|
### Index par Voûte: `vault_{vaultId}`
 | 
						|
 | 
						|
**Configuration Index:**
 | 
						|
```json
 | 
						|
{
 | 
						|
  "primaryKey": "docId",
 | 
						|
  "searchableAttributes": [
 | 
						|
    "title",
 | 
						|
    "content",
 | 
						|
    "headings",
 | 
						|
    "tags",
 | 
						|
    "fileName",
 | 
						|
    "path"
 | 
						|
  ],
 | 
						|
  "filterableAttributes": [
 | 
						|
    "tags",
 | 
						|
    "folder",
 | 
						|
    "ext",
 | 
						|
    "hasAttachment",
 | 
						|
    "mtime",
 | 
						|
    "size"
 | 
						|
  ],
 | 
						|
  "sortableAttributes": [
 | 
						|
    "mtime",
 | 
						|
    "title",
 | 
						|
    "size"
 | 
						|
  ],
 | 
						|
  "rankingRules": [
 | 
						|
    "words",
 | 
						|
    "typo",
 | 
						|
    "proximity",
 | 
						|
    "attribute",
 | 
						|
    "sort",
 | 
						|
    "exactness"
 | 
						|
  ],
 | 
						|
  "typoTolerance": {
 | 
						|
    "enabled": true,
 | 
						|
    "minWordSizeForTypos": {
 | 
						|
      "oneTypo": 4,
 | 
						|
      "twoTypos": 8
 | 
						|
    },
 | 
						|
    "disableOnWords": [],
 | 
						|
    "disableOnAttributes": []
 | 
						|
  },
 | 
						|
  "faceting": {
 | 
						|
    "maxValuesPerFacet": 100
 | 
						|
  },
 | 
						|
  "pagination": {
 | 
						|
    "maxTotalHits": 1000
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Structure Document:**
 | 
						|
```typescript
 | 
						|
interface MeilisearchDocument {
 | 
						|
  docId: string;              // Format: {vaultId}:{relativePath}
 | 
						|
  vaultId: string;            // Ex: "primary", "work", "personal"
 | 
						|
  title: string;              // Note title
 | 
						|
  fileName: string;           // Ex: "home.md"
 | 
						|
  path: string;               // Ex: "folder1/subfolder/note"
 | 
						|
  folder: string;             // Ex: "folder1/subfolder"
 | 
						|
  ext: string;                // Ex: "md"
 | 
						|
  content: string;            // Body markdown (stripped frontmatter)
 | 
						|
  contentPreview: string;     // First 200 chars
 | 
						|
  headings: string[];         // ["# Heading 1", "## Heading 2"]
 | 
						|
  tags: string[];             // ["#project", "#important"]
 | 
						|
  properties: Record<string, any>; // Frontmatter key-value
 | 
						|
  hasAttachment: boolean;     // Contains ![[image.png]]
 | 
						|
  linkCount: number;          // Number of outgoing links
 | 
						|
  backlinksCount: number;     // Number of incoming links
 | 
						|
  mtime: number;              // Unix timestamp
 | 
						|
  size: number;               // Bytes
 | 
						|
  createdAt: string;          // ISO 8601
 | 
						|
  updatedAt: string;          // ISO 8601
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 3. Mapping Opérateurs Obsidian → Meilisearch
 | 
						|
 | 
						|
| Opérateur Obsidian | Meilisearch Query/Filter | Exemple |
 | 
						|
|--------------------|--------------------------|---------|
 | 
						|
| `tag:#project` | `filter: "tags = '#project'"` | `?filter=tags%20%3D%20%27%23project%27` |
 | 
						|
| `path:folder1/` | `filter: "folder = 'folder1'"` | `?filter=folder%20%3D%20%27folder1%27` |
 | 
						|
| `file:home` | `filter: "fileName = 'home.md'"` | `?filter=fileName%20%3D%20%27home.md%27` |
 | 
						|
| `-tag:#archive` | `filter: "tags != '#archive'"` | `?filter=tags%20!%3D%20%27%23archive%27` |
 | 
						|
| `content:search term` | `q=search term&attributesToSearchOn=content` | Default search |
 | 
						|
| `section:"My Heading"` | `q=My Heading&attributesToSearchOn=headings` | Section-specific |
 | 
						|
| `task:TODO` | Custom backend filter (tasks extracted) | - |
 | 
						|
| `has:attachment` | `filter: "hasAttachment = true"` | `?filter=hasAttachment%20%3D%20true` |
 | 
						|
 | 
						|
**Query combinée exemple:**
 | 
						|
```
 | 
						|
Obsidian: tag:#project -tag:#archive path:work/ important
 | 
						|
 | 
						|
Meilisearch:
 | 
						|
POST /indexes/vault_primary/search
 | 
						|
{
 | 
						|
  "q": "important",
 | 
						|
  "filter": [
 | 
						|
    "tags = '#project'",
 | 
						|
    "tags != '#archive'",
 | 
						|
    "folder = 'work'"
 | 
						|
  ],
 | 
						|
  "attributesToHighlight": ["content", "title"],
 | 
						|
  "highlightPreTag": "<mark>",
 | 
						|
  "highlightPostTag": "</mark>",
 | 
						|
  "limit": 50
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 4. Routes API Backend
 | 
						|
 | 
						|
### `/api/search` - Recherche unifiée
 | 
						|
**Endpoint:** `POST /api/search`
 | 
						|
 | 
						|
**Request:**
 | 
						|
```typescript
 | 
						|
interface SearchRequest {
 | 
						|
  query: string;               // Raw Obsidian query
 | 
						|
  vaultId?: string;            // Default: "primary"
 | 
						|
  options?: {
 | 
						|
    limit?: number;            // Default: 50
 | 
						|
    offset?: number;           // Pagination
 | 
						|
    attributesToRetrieve?: string[];
 | 
						|
    attributesToHighlight?: string[];
 | 
						|
  };
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Response:**
 | 
						|
```typescript
 | 
						|
interface SearchResponse {
 | 
						|
  hits: Array<{
 | 
						|
    docId: string;
 | 
						|
    title: string;
 | 
						|
    path: string;
 | 
						|
    _formatted?: {             // With highlights
 | 
						|
      title: string;
 | 
						|
      content: string;
 | 
						|
    };
 | 
						|
    _matchesPosition?: MatchRange[];
 | 
						|
  }>;
 | 
						|
  estimatedTotalHits: number;
 | 
						|
  processingTimeMs: number;
 | 
						|
  query: string;
 | 
						|
  facetDistribution?: Record<string, Record<string, number>>;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
### `/api/search/suggest` - Autocomplete
 | 
						|
**Endpoint:** `GET /api/search/suggest?q=term&type=tag`
 | 
						|
 | 
						|
**Response:**
 | 
						|
```typescript
 | 
						|
interface SuggestResponse {
 | 
						|
  suggestions: string[];
 | 
						|
  type: 'tag' | 'file' | 'path' | 'property';
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
### `/api/search/facets` - Facettes disponibles
 | 
						|
**Endpoint:** `GET /api/search/facets`
 | 
						|
 | 
						|
**Response:**
 | 
						|
```typescript
 | 
						|
interface FacetsResponse {
 | 
						|
  tags: Array<{ name: string; count: number }>;
 | 
						|
  folders: Array<{ name: string; count: number }>;
 | 
						|
  extensions: Array<{ name: string; count: number }>;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
### `/api/search/reindex` - Réindexation manuelle
 | 
						|
**Endpoint:** `POST /api/search/reindex`
 | 
						|
 | 
						|
**Request:**
 | 
						|
```typescript
 | 
						|
interface ReindexRequest {
 | 
						|
  vaultId: string;
 | 
						|
  full?: boolean;              // true = rebuild complet, false = delta
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Response:**
 | 
						|
```typescript
 | 
						|
interface ReindexResponse {
 | 
						|
  taskId: string;              // Meilisearch task UID
 | 
						|
  status: 'enqueued' | 'processing' | 'succeeded' | 'failed';
 | 
						|
  indexedDocuments?: number;
 | 
						|
  durationMs?: number;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
### `/api/log` - Logging endpoint
 | 
						|
**Endpoint:** `POST /api/log`
 | 
						|
 | 
						|
**Request:**
 | 
						|
```typescript
 | 
						|
interface LogBatchRequest {
 | 
						|
  records: LogRecord[];
 | 
						|
}
 | 
						|
 | 
						|
interface LogRecord {
 | 
						|
  ts: string;                  // ISO 8601
 | 
						|
  level: 'debug' | 'info' | 'warn' | 'error';
 | 
						|
  app: string;                 // "ObsiViewer"
 | 
						|
  sessionId: string;           // UUID
 | 
						|
  userAgent: string;
 | 
						|
  context: {
 | 
						|
    version: string;
 | 
						|
    route?: string;
 | 
						|
    theme?: 'light' | 'dark';
 | 
						|
    vault?: string;
 | 
						|
  };
 | 
						|
  event: LogEvent;
 | 
						|
  data: Record<string, unknown>;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Response:**
 | 
						|
```typescript
 | 
						|
interface LogResponse {
 | 
						|
  accepted: number;
 | 
						|
  rejected: number;
 | 
						|
  errors?: string[];
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 5. Plan /api/log - Événements Standardisés (12+)
 | 
						|
 | 
						|
### Événements Frontend
 | 
						|
 | 
						|
| Event | Trigger | Data Fields | Exemple |
 | 
						|
|-------|---------|-------------|---------|
 | 
						|
| `APP_START` | App bootstrap | `viewport: {width, height}` | User opens app |
 | 
						|
| `APP_STOP` | beforeunload | - | User closes tab |
 | 
						|
| `PAGE_VIEW` | Route change | `route: string, previousRoute?: string` | Navigate to /graph |
 | 
						|
| `SEARCH_EXECUTED` | Search submit | `query: string, queryLength: number, resultsCount?: number` | User searches "tag:#project" |
 | 
						|
| `SEARCH_OPTIONS_APPLIED` | Filter toggle | `options: {caseSensitive, regex, wholeWord}` | Enable case-sensitive |
 | 
						|
| `GRAPH_VIEW_OPEN` | Graph tab click | - | User opens graph view |
 | 
						|
| `GRAPH_INTERACTION` | Node click/hover | `action: 'click'\|'hover', nodeId: string` | Click node in graph |
 | 
						|
| `BOOKMARKS_OPEN` | Bookmarks panel | - | Open bookmarks |
 | 
						|
| `BOOKMARKS_MODIFY` | Add/edit/delete | `action: 'add'\|'update'\|'delete', path: string` | Bookmark note |
 | 
						|
| `CALENDAR_SEARCH_EXECUTED` | Calendar date select | `resultsCount: number, dateRange?: string` | Select date range |
 | 
						|
| `ERROR_BOUNDARY` | Uncaught error | `message: string, stack?: string, componentStack?: string` | React error boundary |
 | 
						|
| `PERFORMANCE_METRIC` | Web Vitals | `metric: 'LCP'\|'FID'\|'CLS', value: number` | Lighthouse metrics |
 | 
						|
 | 
						|
### Événements Backend (à implémenter)
 | 
						|
 | 
						|
| Event | Trigger | Data Fields |
 | 
						|
|-------|---------|-------------|
 | 
						|
| `VAULT_INDEXED` | Meilisearch indexation complète | `vaultId: string, documentsCount: number, durationMs: number` |
 | 
						|
| `VAULT_WATCH_ERROR` | Chokidar error | `vaultId: string, error: string` |
 | 
						|
| `SEARCH_BACKEND_EXECUTED` | Meilisearch query | `query: string, hitsCount: number, processingTimeMs: number` |
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 6. Stratégie Worker/WebGL pour Graph - Critères Anti-Gel
 | 
						|
 | 
						|
### Problème actuel
 | 
						|
- Graph layout calcul dans Web Worker ✅ (bon)
 | 
						|
- Rendu Canvas dans main thread ❌ (peut bloquer)
 | 
						|
- Pas de LOD (Level of Detail) ❌
 | 
						|
- Pas de capping nodes/links ❌
 | 
						|
 | 
						|
### Solution cible
 | 
						|
 | 
						|
#### A) Web Worker pour Layout (déjà implémenté)
 | 
						|
- `graph-layout.worker.ts` calcule positions avec d3-force
 | 
						|
- Communication via `postMessage` ✅
 | 
						|
- **Garde-fou:** Timeout 30s, max iterations 1000
 | 
						|
 | 
						|
#### B) Canvas Rendering Optimisé
 | 
						|
 | 
						|
**Throttle redraw:**
 | 
						|
```typescript
 | 
						|
private lastDrawTime = 0;
 | 
						|
private readonly MIN_FRAME_INTERVAL = 16; // 60fps max
 | 
						|
 | 
						|
private scheduleRedraw(): void {
 | 
						|
  const now = performance.now();
 | 
						|
  if (now - this.lastDrawTime < this.MIN_FRAME_INTERVAL) {
 | 
						|
    return; // Skip frame
 | 
						|
  }
 | 
						|
  requestAnimationFrame(() => {
 | 
						|
    this.draw();
 | 
						|
    this.lastDrawTime = performance.now();
 | 
						|
  });
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**LOD (Level of Detail):**
 | 
						|
```typescript
 | 
						|
private draw(): void {
 | 
						|
  const zoom = this.transform().k;
 | 
						|
  
 | 
						|
  // Adaptive rendering based on zoom
 | 
						|
  if (zoom < 0.5) {
 | 
						|
    // Far view: circles only, no labels, no arrows
 | 
						|
    this.drawNodesSimple(ctx, nodes);
 | 
						|
  } else if (zoom < 1.5) {
 | 
						|
    // Medium: circles + labels
 | 
						|
    this.drawNodes(ctx, nodes);
 | 
						|
    this.drawLabels(ctx, nodes);
 | 
						|
  } else {
 | 
						|
    // Close: full detail
 | 
						|
    this.drawLinks(ctx, links, settings);
 | 
						|
    this.drawNodes(ctx, nodes);
 | 
						|
    this.drawLabels(ctx, nodes);
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Capping nodes/links:**
 | 
						|
```typescript
 | 
						|
private readonly MAX_VISIBLE_NODES = 500;
 | 
						|
private readonly MAX_VISIBLE_LINKS = 1000;
 | 
						|
 | 
						|
private cullNodes(nodes: SimulationNode[]): SimulationNode[] {
 | 
						|
  // Show only visible nodes in viewport + margin
 | 
						|
  const visible = nodes.filter(n => this.isInViewport(n));
 | 
						|
  
 | 
						|
  if (visible.length > this.MAX_VISIBLE_NODES) {
 | 
						|
    // Sort by importance (link count, selection state)
 | 
						|
    return visible
 | 
						|
      .sort((a, b) => b.importance - a.importance)
 | 
						|
      .slice(0, this.MAX_VISIBLE_NODES);
 | 
						|
  }
 | 
						|
  
 | 
						|
  return visible;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Clustering pour grands graphes (>1000 nodes):**
 | 
						|
```typescript
 | 
						|
interface ClusterNode {
 | 
						|
  id: string;
 | 
						|
  type: 'cluster';
 | 
						|
  children: string[]; // Node IDs in cluster
 | 
						|
  x: number;
 | 
						|
  y: number;
 | 
						|
  radius: number;
 | 
						|
}
 | 
						|
 | 
						|
// Use force-cluster layout for >1000 nodes
 | 
						|
if (nodes.length > 1000) {
 | 
						|
  const clusters = this.clusterByFolder(nodes, settings.clusterThreshold);
 | 
						|
  this.renderClusters(clusters);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
**Critères d'arrêt du gel UI:**
 | 
						|
- ✅ Aucune frame >50ms (pas de janky scroll)
 | 
						|
- ✅ Redraw budget: <16ms par frame (60fps)
 | 
						|
- ✅ Worker layout timeout: 30s max
 | 
						|
- ✅ Progressive rendering: 100 nodes/frame si >500 total
 | 
						|
- ✅ User interaction priority: stopper animation si clic détecté
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
## 7. Docker Multi-Stage + Healthcheck
 | 
						|
 | 
						|
**Dockerfile optimisé:**
 | 
						|
```dockerfile
 | 
						|
# syntax=docker/dockerfile:1
 | 
						|
 | 
						|
# ========== Stage 1: Builder ==========
 | 
						|
FROM node:20-alpine AS builder
 | 
						|
 | 
						|
WORKDIR /build
 | 
						|
 | 
						|
# Install dependencies (leverage layer cache)
 | 
						|
COPY package*.json ./
 | 
						|
RUN npm ci --only=production=false
 | 
						|
 | 
						|
# Copy source
 | 
						|
COPY . .
 | 
						|
 | 
						|
# Build Angular (production)
 | 
						|
RUN npx ng build --configuration=production
 | 
						|
 | 
						|
# Prune dev dependencies
 | 
						|
RUN npm prune --omit=dev
 | 
						|
 | 
						|
# ========== Stage 2: Runtime ==========
 | 
						|
FROM node:20-alpine AS runtime
 | 
						|
 | 
						|
# Security: non-root user
 | 
						|
RUN addgroup -g 1001 -S nodejs && \
 | 
						|
    adduser -S nodejs -u 1001
 | 
						|
 | 
						|
WORKDIR /app
 | 
						|
 | 
						|
# Install curl for healthcheck
 | 
						|
RUN apk add --no-cache curl
 | 
						|
 | 
						|
# Copy artifacts from builder
 | 
						|
COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist
 | 
						|
COPY --from=builder --chown=nodejs:nodejs /build/server ./server
 | 
						|
COPY --from=builder --chown=nodejs:nodejs /build/node_modules ./node_modules
 | 
						|
COPY --from=builder --chown=nodejs:nodejs /build/package*.json ./
 | 
						|
COPY --from=builder --chown=nodejs:nodejs /build/db ./db
 | 
						|
 | 
						|
# Create volumes
 | 
						|
RUN mkdir -p /app/vault /app/tmp /app/logs && \
 | 
						|
    chown -R nodejs:nodejs /app
 | 
						|
 | 
						|
USER nodejs
 | 
						|
 | 
						|
# Expose port
 | 
						|
EXPOSE 4000
 | 
						|
 | 
						|
# Healthcheck
 | 
						|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
 | 
						|
  CMD curl -f http://localhost:4000/api/health || exit 1
 | 
						|
 | 
						|
# Environment defaults
 | 
						|
ENV NODE_ENV=production \
 | 
						|
    PORT=4000 \
 | 
						|
    VAULT_PATH=/app/vault \
 | 
						|
    LOG_LEVEL=info
 | 
						|
 | 
						|
# Start server
 | 
						|
CMD ["node", "./server/index.mjs"]
 | 
						|
```
 | 
						|
 | 
						|
**Variables d'env clés:**
 | 
						|
```bash
 | 
						|
# .env.example
 | 
						|
PORT=4000
 | 
						|
NODE_ENV=production
 | 
						|
VAULT_PATH=/app/vault
 | 
						|
VAULT_ID=primary
 | 
						|
MEILISEARCH_URL=http://meilisearch:7700
 | 
						|
MEILISEARCH_KEY=masterKey
 | 
						|
LOG_LEVEL=info
 | 
						|
LOG_ENDPOINT=http://localhost:4000/api/log
 | 
						|
ENABLE_SEARCH_CACHE=true
 | 
						|
CACHE_TTL_SECONDS=300
 | 
						|
```
 | 
						|
 |