Bläddra i källkod

chore(dashboard): Implementation plan for global search

David Höck 5 månader sedan
förälder
incheckning
0ae4055518
1 ändrade filer med 1502 tillägg och 0 borttagningar
  1. 1502 0
      GLOBAL_SEARCH_IMPLEMENTATION_PLAN.md

+ 1502 - 0
GLOBAL_SEARCH_IMPLEMENTATION_PLAN.md

@@ -0,0 +1,1502 @@
+# Global Search Implementation Plan
+
+## Overview
+
+This document outlines the comprehensive implementation plan for adding a global search feature to the Vendure dashboard under `packages/dashboard`, similar to modern command palette interfaces.
+
+## Architecture Overview
+
+The search feature will be built as:
+
+- **Backend**: GraphQL API extension in the Dashboard Plugin
+- **Frontend**: React component integrated into the AppLayout header
+- **Search UI**: Command palette using existing `cmdk` components from our shadcn components
+- **Indexing**: Scheduled indexing system with pluggable search backends (Database, TypeSense, etc.)
+
+## Searchable Content Categories
+
+### Core Built-in Entities
+
+All Vendure built-in entities should be searchable:
+
+**Catalog Entities:**
+
+- **Products** - Name, SKU, description, variant names, translations
+- **Product Variants** - Name, SKU, price, stock status
+- **Collections** - Name, description, slug, translations
+- **Assets** - Filename, name, alt text, type
+- **Facets & Facet Values** - Name, code, translations
+- **Tags** - Value
+
+**Customer Management:**
+
+- **Customers** - First name, last name, email, phone number
+- **Customer Groups** - Name, code
+- **Addresses** - Full address, company, contact info
+
+**Order Management:**
+
+- **Orders** - Code, customer info, state, total, items
+- **Order Lines** - Product names, SKUs, quantities
+- **Payments** - Method, amount, state, transaction ID
+- **Fulfillments** - Method, tracking code, state
+- **Refunds** - Reason, amount, state
+
+**Configuration:**
+
+- **Channels** - Name, code, description, hostname
+- **Countries** - Name, code, enabled status
+- **Provinces** - Name, code, country
+- **Zones** - Name, members
+- **Tax Categories** - Name, is default
+- **Tax Rates** - Name, value, zone, category
+- **Shipping Methods** - Name, code, description, translations
+- **Payment Methods** - Name, code, description, translations
+
+**System & Access:**
+
+- **Administrators** - First name, last name, email, role
+- **Roles** - Code, description, permissions
+- **Users** - Identifier, verified status
+- **Sessions** - Active user sessions
+- **Stock Locations** - Name, description
+
+**Marketing:**
+
+- **Promotions** - Name, coupon code, description, translations
+
+### Custom Entities
+
+Dynamic discovery and indexing of custom entities:
+
+- **Plugin-defined Entities** - Auto-discover entities from installed plugins
+- **Custom Fields** - Index custom field values across all entity types
+- **Entity Extensions** - Search extended entity properties
+
+### Data Mapping for Entities
+
+For each built-in database entity there must be a data mapper in place.
+For custom entities there is the need to provide these entity mappers. Developers must also be able to extend the data mapping for built-in entities, which means that the built-in data mapper gets extended by a provided data mappers.
+The task of a data mapper is to map the entity to the search index item.
+
+### Dashboard Content
+
+- **Navigation Items** - Menu labels, routes, permissions
+- **Settings Pages** - Global settings, channel settings, tax settings, etc.
+- **Recent Activity** - Recently viewed entities, user activity
+- **Quick Actions** - Global and context-aware actions (see detailed section below)
+
+### Settings Pages Coverage
+
+There are settings and systems pages that are not based on list-views, like the job queue or scheduled tasks. These pages are searchable through a quick action that is registered.
+
+### Quick Actions System
+
+A comprehensive quick actions system that includes both global and context-aware actions:
+
+#### Global Quick Actions
+
+Always available actions regardless of current page:
+
+- **Create Actions** - "Create new product", "Create new customer", "Create new order"
+- **Navigation Actions** - "Go to products", "Go to orders", "Go to customers"
+- **Account Actions** - "Go to profile", "Change password", "Logout"
+- **Search Actions** - "Search products", "Search customers", "Search orders"
+- **System Actions** - "View job queue", "Check system health", "View logs"
+
+#### Context-Aware Quick Actions
+
+Actions that appear only on specific pages/contexts:
+
+- **Product Detail Page** - "Save product", "Duplicate product", "Delete product", "Add variant"
+- **Order Detail Page** - "Fulfill order", "Add payment", "Cancel order", "Print invoice"
+- **Customer Detail Page** - "Send email", "Add address", "View orders", "Add to group"
+- **Collection Page** - "Save collection", "Add products", "Move collection"
+- **Any List Page** - "Export data", "Bulk edit", "Filter results", "Sort by..."
+
+#### Custom Actions via `defineDashboardExtensions`
+
+Developers can register custom actions:
+
+- **Global Custom Actions** - Available everywhere
+- **Context-Aware Custom Actions** - Page/route specific
+- **Entity-Specific Actions** - Based on current entity being viewed
+- **Keyboard Shortcuts** - Custom hotkeys for actions
+
+### External Content Sources
+
+- **Documentation** - Help content, API reference, tutorials (from existing search index that powers docs.vendure.io)
+- **Blog Posts** - Articles, guides, announcements from website (Datasource: Storyblok CMS)
+- **Plugins** - Community and official plugins from website (Datasource: Vendure app that powers our website)
+
+## Backend Implementation
+
+### Search Index Architecture
+
+The search system uses a dedicated index that aggregates data from all internal and external sources:
+
+#### Index Structure
+
+```typescript
+interface SearchIndexEntry {
+    id: string;
+    type: SearchResultType;
+    title: string;
+    subtitle?: string;
+    description?: string;
+    content: string; // Full searchable text content
+    url: string;
+    thumbnailUrl?: string;
+    metadata: Record<string, any>;
+    permissions?: string[]; // Required permissions to view
+    channelIds: string[]; // Channel access
+    entityId?: string; // Original entity ID (for internal entities)
+    entityName?: string; // Original entity name
+    lastUpdated: Date;
+    searchVector?: string; // For advanced search backends
+}
+```
+
+#### Indexing Sources
+
+- **Internal Entities** - All Vendure entities (products, orders, customers, etc.)
+- **Custom Entities** - Plugin-defined entities
+- **Settings & Configuration** - Dashboard settings, system config
+- **External Content** - Documentation, blog posts, plugins
+
+### Search Backend Strategies
+
+#### Default Database Strategy
+
+```typescript
+// Default implementation using the selected database (PostgreSQL, MySQL, etc.)
+@Injectable()
+export class DatabaseSearchStrategy implements SearchStrategy {
+    async indexContent(entries: SearchIndexEntry[]): Promise<void> {
+        // Upsert entries into search_index table
+        // Use database full-text search capabilities
+    }
+
+    async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
+        // Use database full-text search with ranking
+        // PostgreSQL: ts_rank, ts_headline
+        // MySQL: MATCH() AGAINST()
+    }
+}
+```
+
+#### TypeSense Strategy
+
+```typescript
+// Alternative implementation for TypeSense
+@Injectable()
+export class TypeSenseSearchStrategy implements SearchStrategy {
+    async indexContent(entries: SearchIndexEntry[]): Promise<void> {
+        // Index into TypeSense collection
+        // Leverage TypeSense's typo tolerance and faceting
+    }
+
+    async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
+        // Use TypeSense search with advanced features
+        // Typo tolerance, faceting, geo-search, etc.
+    }
+}
+```
+
+### GraphQL Schema Extensions
+
+This only extend the Admin API of Vendure.
+
+```typescript
+// packages/dashboard/plugin/api/search-extensions.ts
+type GlobalSearchResult {
+  id: String!
+  type: SearchResultType!
+  title: String!
+  subtitle: String
+  description: String
+  url: String!
+  thumbnailUrl: String
+  metadata: JSON
+  relevanceScore: Float!
+  lastModified: DateTime
+}
+
+enum SearchResultType {
+  # Core Entities
+  PRODUCT
+  PRODUCT_VARIANT
+  CUSTOMER
+  ORDER
+  COLLECTION
+  ADMINISTRATOR
+  CHANNEL
+  ASSET
+  FACET
+  FACET_VALUE
+  PROMOTION
+  PAYMENT_METHOD
+  SHIPPING_METHOD
+  TAX_CATEGORY
+  TAX_RATE
+  COUNTRY
+  ZONE
+  ROLE
+  CUSTOMER_GROUP
+  STOCK_LOCATION
+  TAG
+
+  # Custom/Plugin Entities
+  CUSTOM_ENTITY
+
+  # Dashboard Content
+  NAVIGATION
+  SETTINGS
+  QUICK_ACTION
+
+  # External Content
+  DOCUMENTATION
+  BLOG_POST
+  PLUGIN
+  WEBSITE_CONTENT
+}
+
+input GlobalSearchInput {
+  query: String!
+  types: [SearchResultType!]
+  limit: Int = 20
+  skip: Int = 0
+  channelId: String
+}
+
+extend type Query {
+  globalSearch(input: GlobalSearchInput!): [GlobalSearchResult!]!
+}
+```
+
+### Search Service Implementation
+
+```typescript
+// packages/dashboard/plugin/service/global-search.service.ts
+@Injectable()
+export class GlobalSearchService {
+    constructor(private searchStrategy: SearchStrategy) {}
+
+    async search(input: GlobalSearchInput): Promise<GlobalSearchResult[]> {
+        // Use the configured search strategy to search the index
+        const results = await this.searchStrategy.search(input.query, {
+            types: input.types,
+            limit: input.limit,
+            offset: input.skip,
+            channelId: input.channelId,
+        });
+
+        return results;
+    }
+}
+```
+
+### Search Strategy Interface
+
+```typescript
+// packages/dashboard/plugin/service/search-strategy.interface.ts
+export interface SearchStrategy {
+    // Index content into the search backend
+    indexContent(entries: SearchIndexEntry[]): Promise<void>;
+
+    // Remove content from index
+    removeFromIndex(ids: string[]): Promise<void>;
+
+    // Search the index
+    search(query: string, options: SearchOptions): Promise<SearchResult[]>;
+
+    // Health check for the search backend
+    healthCheck(): Promise<boolean>;
+
+    // Get search statistics
+    getStats(): Promise<SearchStats>;
+}
+
+interface SearchOptions {
+    types?: SearchResultType[];
+    limit?: number;
+    offset?: number;
+    channelId?: string;
+    permissions?: string[];
+}
+
+interface SearchResult {
+    id: string;
+    type: SearchResultType;
+    title: string;
+    subtitle?: string;
+    description?: string;
+    url: string;
+    thumbnailUrl?: string;
+    metadata: Record<string, any>;
+    relevanceScore: number;
+    highlights?: SearchHighlight[];
+}
+
+interface SearchHighlight {
+    field: string;
+    snippet: string;
+}
+
+interface SearchStats {
+    totalEntries: number;
+    lastIndexed: Date;
+    backendType: string;
+    indexSizeBytes?: number;
+}
+```
+
+### Search Indexing Service
+
+```typescript
+// packages/dashboard/plugin/service/search-indexing.service.ts
+@Injectable()
+export class SearchIndexingService {
+    constructor(
+        private searchStrategy: SearchStrategy,
+        private entityDiscoveryService: EntityDiscoveryService,
+        private documentationContentService: DocumentationContentService,
+        private websiteContentService: WebsiteContentService,
+    ) {}
+
+    // Full reindex of all content
+    async fullReindex(): Promise<void> {
+        Logger.info('Starting full search index rebuild');
+
+        const entries: SearchIndexEntry[] = [];
+
+        // Index internal entities
+        entries.push(...(await this.indexInternalEntities()));
+
+        // Index custom entities
+        entries.push(...(await this.indexCustomEntities()));
+
+        // Index external content
+        entries.push(...(await this.indexExternalContent()));
+
+        // Update the search index
+        await this.searchStrategy.indexContent(entries);
+
+        Logger.info(`Search index rebuilt with ${entries.length} entries`);
+    }
+
+    // Incremental update for specific entities
+    async updateEntity(entityType: string, entityId: string): Promise<void> {
+        const entry = await this.createIndexEntryForEntity(entityType, entityId);
+        if (entry) {
+            await this.searchStrategy.indexContent([entry]);
+        }
+    }
+
+    // Remove entity from index
+    async removeEntity(entityType: string, entityId: string): Promise<void> {
+        const indexId = `${entityType}:${entityId}`;
+        await this.searchStrategy.removeFromIndex([indexId]);
+    }
+
+    private async indexInternalEntities(): Promise<SearchIndexEntry[]> {
+        const entries: SearchIndexEntry[] = [];
+
+        // Index all core Vendure entities
+        const entityTypes = [
+            'Product',
+            'ProductVariant',
+            'Customer',
+            'Order',
+            'Collection',
+            'Asset',
+            'Facet',
+            'FacetValue',
+            'Administrator',
+            'Role',
+            'Channel',
+            'Country',
+            'Zone',
+            'TaxCategory',
+            'TaxRate',
+            'PaymentMethod',
+            'ShippingMethod',
+            'Promotion',
+            'StockLocation',
+        ];
+
+        for (const entityType of entityTypes) {
+            entries.push(...(await this.indexEntityType(entityType)));
+        }
+
+        return entries;
+    }
+
+    private async indexEntityType(entityType: string): Promise<SearchIndexEntry[]> {
+        // Implementation specific to each entity type
+        // Extract searchable text, permissions, channels, etc.
+    }
+
+    private async indexExternalContent(): Promise<SearchIndexEntry[]> {
+        const entries: SearchIndexEntry[] = [];
+
+        try {
+            // Index documentation content
+            const docEntries = await this.documentationContentService.fetchDocumentationContent();
+            entries.push(...docEntries);
+
+            // Index blog posts from Storyblok
+            const blogEntries = await this.websiteContentService.fetchBlogPosts();
+            entries.push(...blogEntries);
+
+            // Index plugins
+            const pluginEntries = await this.websiteContentService.fetchPlugins();
+            entries.push(...pluginEntries);
+
+            Logger.info(`Indexed ${entries.length} external content items`);
+        } catch (error) {
+            Logger.error('Failed to index external content', error);
+        }
+
+        return entries;
+    }
+
+    // Method to refresh only external content (can be called separately)
+    async refreshExternalContent(): Promise<void> {
+        Logger.info('Refreshing external content index');
+
+        // Remove existing external content from index
+        await this.searchStrategy.removeFromIndex(['docs:*', 'blog:*', 'plugin:*']);
+
+        // Re-index external content
+        const externalEntries = await this.indexExternalContent();
+        await this.searchStrategy.indexContent(externalEntries);
+
+        Logger.info('External content refresh completed');
+    }
+}
+```
+
+### Scheduled Indexing with ScheduledTask and JobQueue
+
+Using Vendure's built-in scheduling features:
+
+```typescript
+// packages/dashboard/plugin/tasks/search-index-task.ts
+import { ScheduledTask, TaskContext } from '@vendure/core';
+
+export const searchFullReindexTask = new ScheduledTask({
+    name: 'search-full-reindex',
+    cron: cron => cron.daily().atHour(2), // Daily at 2 AM
+    run: async (ctx: TaskContext) => {
+        const searchIndexingService = ctx.injector.get(SearchIndexingService);
+
+        try {
+            await searchIndexingService.fullReindex();
+            ctx.logger.info('Search full reindex completed successfully');
+        } catch (error) {
+            ctx.logger.error('Search full reindex failed', error);
+            throw error;
+        }
+    },
+});
+
+export const searchIncrementalUpdateTask = new ScheduledTask({
+    name: 'search-incremental-update',
+    cron: cron => cron.everyMinutes(15), // Every 15 minutes
+    run: async (ctx: TaskContext) => {
+        const searchIndexingService = ctx.injector.get(SearchIndexingService);
+
+        try {
+            await searchIndexingService.incrementalUpdate();
+            ctx.logger.info('Search incremental update completed');
+        } catch (error) {
+            ctx.logger.error('Search incremental update failed', error);
+            throw error;
+        }
+    },
+});
+
+export const searchExternalContentRefreshTask = new ScheduledTask({
+    name: 'search-external-content-refresh',
+    cron: cron => cron.everyHours(12), // Every 12 hours
+    run: async (ctx: TaskContext) => {
+        const searchIndexingService = ctx.injector.get(SearchIndexingService);
+
+        try {
+            await searchIndexingService.refreshExternalContent();
+            ctx.logger.info('External content refresh completed');
+        } catch (error) {
+            ctx.logger.error('External content refresh failed', error);
+            throw error;
+        }
+    },
+});
+```
+
+### Job Queue Integration
+
+For intensive indexing operations that should run in the background:
+
+```typescript
+// packages/dashboard/plugin/service/search-job.service.ts
+import { Job, JobQueue, JobQueueService } from '@vendure/core';
+
+interface ReindexJobData {
+    entityTypes?: string[];
+    forceFullReindex?: boolean;
+}
+
+@Injectable()
+export class SearchJobService {
+    private jobQueue: JobQueue<ReindexJobData>;
+
+    constructor(
+        private jobQueueService: JobQueueService,
+        private indexingService: SearchIndexingService,
+    ) {
+        this.jobQueue = this.jobQueueService.createQueue({
+            name: 'search-indexing',
+            process: this.processSearchJob.bind(this),
+        });
+    }
+
+    async queueFullReindex(): Promise<Job<ReindexJobData>> {
+        return this.jobQueue.add('full-reindex', {
+            forceFullReindex: true,
+        });
+    }
+
+    async queueEntityReindex(entityTypes: string[]): Promise<Job<ReindexJobData>> {
+        return this.jobQueue.add('entity-reindex', {
+            entityTypes,
+        });
+    }
+
+    private async processSearchJob(job: Job<ReindexJobData>) {
+        const { entityTypes, forceFullReindex } = job.data;
+
+        job.setProgress(0);
+
+        try {
+            if (forceFullReindex) {
+                await this.indexingService.fullReindex();
+            } else if (entityTypes) {
+                await this.indexingService.reindexEntityTypes(entityTypes);
+            } else {
+                await this.indexingService.incrementalUpdate();
+            }
+
+            job.setProgress(100);
+        } catch (error) {
+            Logger.error('Search indexing job failed', error);
+            throw error;
+        }
+    }
+}
+```
+
+### Entity Discovery Service
+
+````typescript
+// packages/dashboard/plugin/service/entity-discovery.service.ts
+@Injectable()
+export class EntityDiscoveryService {
+  async discoverAllEntities(): Promise<EntityMetadata[]> {
+    // Auto-discover all TypeORM entities (built-in + custom)
+    // Extract searchable fields from entity metadata
+    // Handle custom fields dynamically
+    // Generate search configurations per entity
+  }
+
+  async getCustomEntityTypes(): Promise<string[]> {
+    // Identify plugin-defined entities
+    // Return entity names not in core Vendure entities list
+  }
+}
+```
+
+### Search Configuration
+
+```typescript
+// packages/dashboard/plugin/config/search-config.ts
+export interface SearchConfig {
+    // Search backend strategy
+    strategy: 'database' | 'typesense' | 'custom';
+
+    // Strategy-specific options
+    strategyOptions?: {
+        database?: DatabaseSearchOptions;
+        typesense?: TypeSenseSearchOptions;
+        custom?: any;
+    };
+
+    // Indexing configuration
+    indexing: {
+        fullReindexCron: string; // Default: '0 2 * * *'
+        incrementalUpdateCron: string; // Default: '*/15 * * * *'
+        batchSize: number; // Default: 1000
+        maxConcurrency: number; // Default: 5
+    };
+
+    // External content sources
+    externalSources: {
+        documentation: {
+            enabled: boolean;
+            endpoint: string;
+            refreshIntervalHours: number; // Default: 24
+        };
+        website: {
+            enabled: boolean;
+            endpoint: string;
+            refreshIntervalHours: number; // Default: 12
+        };
+    };
+}
+
+interface DatabaseSearchOptions {
+    // Full-text search configuration for different databases
+    postgresql?: {
+        searchConfig: string; // Default: 'english'
+        enableHighlighting: boolean;
+    };
+    mysql?: {
+        minWordLength: number;
+        enableBooleanMode: boolean;
+    };
+}
+
+interface TypeSenseSearchOptions {
+    nodes: Array<{
+        host: string;
+        port: number;
+        protocol: 'http' | 'https';
+    }>;
+    apiKey: string;
+    connectionTimeoutSeconds?: number;
+    collectionName?: string; // Default: 'vendure_search'
+}
+```
+
+### Real-time Index Updates
+
+```typescript
+// packages/dashboard/plugin/service/search-event-handler.service.ts
+@Injectable()
+export class SearchEventHandlerService implements OnApplicationBootstrap {
+    constructor(
+        private eventBus: EventBus,
+        private indexingService: SearchIndexingService,
+    ) {}
+
+    onApplicationBootstrap() {
+        // Listen for entity events to keep index up-to-date
+        this.eventBus.ofType(VendureEntityEvent).subscribe(event => {
+            this.handleEntityEvent(event);
+        });
+    }
+
+    private async handleEntityEvent(event: VendureEntityEvent<any>) {
+        const { entity, type, ctx } = event;
+
+        try {
+            switch (type) {
+                case 'created':
+                case 'updated':
+                    await this.indexingService.updateEntity(
+                        entity.constructor.name,
+                        entity.id
+                    );
+                    break;
+
+                case 'deleted':
+                    await this.indexingService.removeEntity(
+                        entity.constructor.name,
+                        event.entity.id
+                    );
+                    break;
+            }
+        } catch (error) {
+            Logger.error(`Failed to update search index for ${entity.constructor.name}:${entity.id}`, error);
+        }
+    }
+}
+```
+
+### External Content Integration
+
+External content is fetched from APIs and indexed locally, so search requests don't hit external services:
+
+#### Documentation Content Service
+
+```typescript
+// packages/dashboard/plugin/service/documentation-content.service.ts
+@Injectable()
+export class DocumentationContentService {
+    private readonly DOCS_API_ENDPOINT = 'https://docs.vendure.io/api/content';
+
+    async fetchDocumentationContent(): Promise<SearchIndexEntry[]> {
+        try {
+            // Fetch all documentation pages from API
+            const response = await fetch(this.DOCS_API_ENDPOINT);
+            const docs = await response.json();
+
+            return docs.map(doc => ({
+                id: `docs:${doc.slug}`,
+                type: 'DOCUMENTATION',
+                title: doc.title,
+                subtitle: doc.category,
+                description: doc.excerpt,
+                content: doc.content, // Full searchable text
+                url: `https://docs.vendure.io/${doc.slug}`,
+                thumbnailUrl: doc.imageUrl,
+                metadata: {
+                    category: doc.category,
+                    tags: doc.tags,
+                    lastModified: doc.updatedAt,
+                },
+                permissions: [], // Public content
+                channelIds: [], // Available to all channels
+                lastUpdated: new Date(),
+            }));
+        } catch (error) {
+            Logger.error('Failed to fetch documentation content', error);
+            return [];
+        }
+    }
+}
+```
+
+#### Website Content Service
+
+```typescript
+// packages/dashboard/plugin/service/website-content.service.ts
+@Injectable()
+export class WebsiteContentService {
+    private readonly STORYBLOK_API_ENDPOINT = 'https://api.storyblok.com/v2/cdn/stories';
+    private readonly STORYBLOK_TOKEN = process.env.STORYBLOK_TOKEN;
+    private readonly VENDURE_PLUGINS_API = 'https://vendure.io/api/plugins';
+
+    async fetchBlogPosts(): Promise<SearchIndexEntry[]> {
+        try {
+            // Fetch blog posts from Storyblok CMS
+            const response = await fetch(
+                `${this.STORYBLOK_API_ENDPOINT}?starts_with=blog&token=${this.STORYBLOK_TOKEN}`
+            );
+            const { stories } = await response.json();
+
+            return stories.map(story => ({
+                id: `blog:${story.slug}`,
+                type: 'BLOG_POST',
+                title: story.content.title,
+                subtitle: `By ${story.content.author}`,
+                description: story.content.excerpt,
+                content: story.content.body, // Full blog content for search
+                url: `https://vendure.io/blog/${story.slug}`,
+                thumbnailUrl: story.content.featuredImage?.filename,
+                metadata: {
+                    author: story.content.author,
+                    publishedAt: story.published_at,
+                    tags: story.tag_list,
+                    readingTime: story.content.readingTime,
+                },
+                permissions: [], // Public content
+                channelIds: [], // Available to all channels
+                lastUpdated: new Date(story.published_at),
+            }));
+        } catch (error) {
+            Logger.error('Failed to fetch blog posts from Storyblok', error);
+            return [];
+        }
+    }
+
+    async fetchPlugins(): Promise<SearchIndexEntry[]> {
+        try {
+            // Fetch plugins from Vendure website API
+            const response = await fetch(this.VENDURE_PLUGINS_API);
+            const plugins = await response.json();
+
+            return plugins.map(plugin => ({
+                id: `plugin:${plugin.id}`,
+                type: 'PLUGIN',
+                title: plugin.name,
+                subtitle: `v${plugin.version} by ${plugin.author}`,
+                description: plugin.description,
+                content: `${plugin.name} ${plugin.description} ${plugin.readme}`,
+                url: `https://vendure.io/plugins/${plugin.slug}`,
+                thumbnailUrl: plugin.logoUrl,
+                metadata: {
+                    author: plugin.author,
+                    version: plugin.version,
+                    downloads: plugin.downloads,
+                    stars: plugin.stars,
+                    category: plugin.category,
+                    compatibility: plugin.vendureVersion,
+                },
+                permissions: [], // Public content
+                channelIds: [], // Available to all channels
+                lastUpdated: new Date(plugin.updatedAt),
+            }));
+        } catch (error) {
+            Logger.error('Failed to fetch plugins', error);
+            return [];
+        }
+    }
+}
+
+## Frontend Implementation
+
+### Search Component Structure
+
+```
+
+src/lib/components/global-search/
+├── global-search-trigger.tsx // Search button/shortcut in header
+├── global-search-dialog.tsx // Main search interface
+├── search-results-list.tsx // Results container
+├── search-result-item.tsx // Individual result component
+├── search-filters.tsx // Type/category filters
+├── search-history.tsx // Recent searches
+├── quick-actions-list.tsx // Quick actions display (Frontend only)
+├── quick-action-item.tsx // Individual quick action
+└── hooks/
+├── use-global-search.ts // Search API integration
+├── use-search-history.ts // Local search history
+├── use-search-shortcuts.ts // Keyboard shortcuts
+└── use-quick-actions.ts // Quick actions management (Frontend only)
+
+````
+
+### Quick Actions (Frontend Only)
+
+Quick actions are managed entirely in the React application and do not go through the GraphQL API:
+
+```typescript
+// src/lib/components/global-search/hooks/use-quick-actions.ts
+export interface QuickAction {
+    id: string;
+    label: string;
+    description?: string;
+    icon?: string;
+    shortcut?: string;
+    isContextAware: boolean;
+    requiredPermissions?: string[];
+    handler: QuickActionHandler;
+    params?: Record<string, any>;
+}
+
+export type QuickActionHandler = (context: QuickActionContext) => void | Promise<void>;
+
+export interface QuickActionContext {
+    // Current page context
+    currentRoute: string;
+    currentEntityType?: string;
+    currentEntityId?: string;
+
+    // Available utilities
+    navigate: (path: string) => void;
+    showNotification: (message: string, type?: 'success' | 'error' | 'warning') => void;
+    confirm: (message: string) => Promise<boolean>;
+
+    // Data access (if needed)
+    executeApiOperation: (query: string, variables?: any) => Promise<any>;
+}
+
+export const useQuickActions = () => {
+    const [globalActions, setGlobalActions] = useState<QuickAction[]>([]);
+    const [contextActions, setContextActions] = useState<QuickAction[]>([]);
+    const location = useLocation();
+    const navigate = useNavigate();
+
+    // Built-in global actions
+    const builtInGlobalActions: QuickAction[] = [
+        {
+            id: 'create-product',
+            label: 'Create New Product',
+            icon: 'plus',
+            shortcut: 'ctrl+shift+p',
+            isContextAware: false,
+            handler: context => context.navigate('/products/new'),
+        },
+        {
+            id: 'create-customer',
+            label: 'Create New Customer',
+            icon: 'user-plus',
+            shortcut: 'ctrl+shift+c',
+            isContextAware: false,
+            handler: context => context.navigate('/customers/new'),
+        },
+        // ... more global actions
+    ];
+
+    // Built-in context-aware actions
+    const getContextActions = (route: string, entityType?: string) => {
+        const actions: QuickAction[] = [];
+
+        if (route.includes('/products/') && entityType === 'product') {
+            actions.push({
+                id: 'save-product',
+                label: 'Save Product',
+                icon: 'save',
+                shortcut: 'ctrl+s',
+                isContextAware: true,
+                handler: async context => {
+                    // Trigger save action for current product
+                    // This would integrate with the product form
+                },
+            });
+        }
+
+        return actions;
+    };
+
+    // Register custom actions from extensions
+    const registerCustomAction = (action: QuickAction) => {
+        if (action.isContextAware) {
+            setContextActions(prev => [...prev, action]);
+        } else {
+            setGlobalActions(prev => [...prev, action]);
+        }
+    };
+
+    return {
+        globalActions: [...builtInGlobalActions, ...globalActions],
+        contextActions: [...getContextActions(location.pathname), ...contextActions],
+        registerCustomAction,
+    };
+};
+```
+
+### Integration Points
+
+- **AppLayout Header**: Search trigger button (🔍 + Cmd/Ctrl+K)
+- **Keyboard Shortcuts**: Global hotkey handling + custom action shortcuts
+- **Navigation**: Direct routing to search results
+- **Context Awareness**: Pre-filter based on current page + show relevant quick actions
+- **Extension API**: Integration with `defineDashboardExtensions` for custom actions
+- **Page Components**: Context-aware actions registration from individual pages
+
+## User Experience Design
+
+### Search Interface
+
+- **Trigger**: Prominent search icon in header
+- **Modal**: Full-screen overlay with cmdk-style interface
+- **Input**: Auto-focus with placeholder suggestions
+- **Shortcuts**: Cmd/Ctrl+K to open, Esc to close
+- **Loading States**: Skeleton loaders, progressive results
+
+### Result Categories
+
+**Quick Actions Section (Always at Top):**
+
+- **Global Actions** - Create entities, navigate, account actions
+- **Context Actions** - Save, duplicate, delete (when on detail pages)
+- **Custom Actions** - Developer-registered actions via extensions
+
+**Search Results:**
+
+- **Recent Items** - Recently viewed/edited entities
+- **Products** - With thumbnails, SKUs, status
+- **Customers** - With avatars, contact info
+- **Orders** - With status badges, customer names
+- **Content** - Collections, promotions, assets
+- **System** - Settings, admin tools, reports
+- **Help** - Documentation, tutorials
+- **Blog** - Articles, guides, announcements
+- **Plugins** - Community and official plugins
+- **Resources** - Website content, guides
+
+### Result Actions
+
+**Quick Actions:**
+
+- **Execute**: Run the action immediately (with keyboard shortcuts)
+- **Parameters**: Some actions may require parameters/confirmation
+
+**Entity Results:**
+
+- **Primary**: Navigate to entity detail page
+- **Secondary**: Quick actions (edit, duplicate, archive)
+- **Contextual**: Entity-specific actions in dropdown
+
+**External Content:**
+
+- **Primary**: Open in new tab
+- **Secondary**: Copy link, bookmark
+
+## Technical Implementation Plan
+
+### Phase 1: Core Infrastructure (Week 1-2)
+
+#### 1. Backend API Setup & Indexing Infrastructure
+
+- Create search GraphQL schema extensions
+- Implement search strategy interface and database strategy
+- Set up search index table/collection structure
+- Implement basic search indexing service
+- Create scheduled indexing tasks (full + incremental)
+- Add search resolver with permission filtering
+
+#### 2. Frontend Foundation & Quick Actions
+
+- Create search dialog component using existing Command UI
+- Implement search hook with GraphQL integration
+- Add keyboard shortcut handling (global + custom action shortcuts)
+- Create basic result list with entity routing
+- Implement quick actions service and components
+- Set up context-aware action registration system
+
+### Phase 2: Entity Integration (Week 3-4)
+
+#### 3. Core Entity Indexing & Search Implementation
+
+**Initial Indexing (High Priority):**
+
+- Implement indexing for core entities: Products, Customers, Orders, Collections
+- Set up entity event handlers for real-time index updates
+- Create entity-specific index entry builders
+- Test search functionality with basic relevance scoring
+
+**Extended Entity Indexing:**
+
+- Index remaining built-in entities (Assets, Facets, Administrators, etc.)
+- Implement custom field indexing
+- Add support for multi-language content
+- Optimize search relevance and ranking
+
+#### 4. UI Enhancement & Built-in Quick Actions
+
+- Result categorization and grouping with quick actions at top
+- Thumbnail/avatar display for entities
+- Status badges and metadata display
+- Search result actions (view, edit, duplicate)
+- Implement built-in global actions (create, navigate, account)
+- Implement built-in context-aware actions (save, duplicate, delete)
+
+### Phase 3: Advanced Features (Week 5-6)
+
+#### 5. Search Optimization & Alternative Backend Support
+
+- Implement TypeSense search strategy
+- Add search highlighting and snippets
+- Create search filters by entity type and metadata
+- Add recent items prioritization and search history
+- Performance optimization and query tuning
+
+#### 6. Custom Actions API & Polish
+
+- Implement `defineDashboardExtensions` API integration
+- Custom action registration and validation system
+- Result caching and pagination improvements
+- Search analytics and usage tracking
+- Accessibility improvements (ARIA, keyboard nav)
+- Mobile responsive design
+
+### Phase 4: Extended Coverage (Week 7-8)
+
+#### 7. Complete Entity Coverage
+
+**Remaining Built-in Entities:**
+
+- Stock Locations, Tags, Global Settings
+- History Entries, Sessions, Users
+- Order Lines, Payments, Fulfillments, Refunds
+- Addresses, Customer Groups
+
+**Settings Pages Integration:**
+
+- Global settings, channel settings, tax configuration
+- System health, job queue, scheduled tasks
+- Navigation menu items and quick actions
+
+#### 8. Custom Entity & External Content Integration
+
+**Custom Entity Discovery:**
+
+- Auto-discovery of plugin-defined entities
+- Dynamic indexing of custom fields
+- Entity extension search support
+
+**External Content Integration:**
+
+- Documentation search API integration
+- Blog posts and website content (Storyblok CMS)
+- Plugin directory integration (Vendure app)
+- Content caching and refresh strategies
+
+#### 9. Advanced Search Features
+
+- Search syntax (quotes, operators, filters)
+- Saved searches and bookmarks
+- Search scoping by channel/context
+- Search result preview/quick view
+
+## Performance Considerations
+
+### Indexing Performance
+
+- **Scheduled Indexing**: Full reindex daily, incremental updates every 15 minutes
+- **Batch Processing**: Process entities in batches (default 1000) to avoid memory issues
+- **Concurrent Processing**: Parallel indexing with configurable concurrency
+- **Real-time Updates**: Entity events trigger immediate index updates
+- **Index Optimization**: Database-specific optimizations (PostgreSQL ts_vector, MySQL FULLTEXT)
+
+### Search Performance
+
+- **Index-based Search**: All searches query the dedicated search index
+- **Search Strategy Abstraction**: Pluggable backends (Database, TypeSense, Elasticsearch)
+- **Result Caching**: Cache frequent searches client-side
+- **Search Debouncing**: 300ms delay to reduce API calls
+- **Pagination**: Efficient offset-based pagination with limits
+- **Permission Filtering**: Index stores required permissions for efficient filtering
+
+### Backend-Specific Optimizations
+
+- **Database Strategy**: Leverage native full-text search (ts_rank for PostgreSQL, MATCH AGAINST for MySQL)
+- **TypeSense Strategy**: Advanced features like typo tolerance, faceting, geo-search
+- **Memory Usage**: Configurable batch sizes and connection pooling
+- **External Content Caching**: Cache with TTL, configurable refresh intervals
+
+## Security & Permissions
+
+- **Permission Filtering**: Results filtered by user permissions
+- **Channel Scoping**: Respect channel access restrictions
+- **Sensitive Data**: Exclude private/internal information
+- **Rate Limiting**: Prevent search API abuse
+
+## Testing Strategy
+
+- **Unit Tests**: Search service logic, result formatting
+- **Integration Tests**: GraphQL API endpoints, permissions
+- **E2E Tests**: Full search workflow, keyboard shortcuts
+- **Performance Tests**: Search response times, large datasets
+
+## Success Metrics
+
+- **Usage**: Search queries per user per session
+- **Performance**: Average search response time < 300ms
+- **Effectiveness**: Click-through rate on search results
+- **User Satisfaction**: Search success rate, user feedback
+
+## File Structure Impact
+
+### New Files to Create
+
+```
+
+packages/dashboard/plugin/api/search-extensions.ts
+packages/dashboard/plugin/service/global-search.service.ts
+packages/dashboard/plugin/service/search-indexing.service.ts
+packages/dashboard/plugin/service/search-job.service.ts
+packages/dashboard/plugin/service/search-event-handler.service.ts
+packages/dashboard/plugin/service/entity-discovery.service.ts
+packages/dashboard/plugin/service/documentation-content.service.ts
+packages/dashboard/plugin/service/website-content.service.ts
+packages/dashboard/plugin/strategies/database-search.strategy.ts
+packages/dashboard/plugin/strategies/typesense-search.strategy.ts
+packages/dashboard/plugin/tasks/search-index-task.ts
+packages/dashboard/plugin/resolver/global-search.resolver.ts
+packages/dashboard/plugin/config/search-config.ts
+packages/dashboard/src/lib/components/global-search/
+├── global-search-trigger.tsx
+├── global-search-dialog.tsx
+├── search-results-list.tsx
+├── search-result-item.tsx
+├── search-filters.tsx
+├── search-history.tsx
+├── quick-actions-list.tsx
+├── quick-action-item.tsx
+└── hooks/
+├── use-global-search.ts
+├── use-search-history.ts
+├── use-search-shortcuts.ts
+└── use-quick-actions.ts
+
+```
+
+### Files to Modify
+
+- `packages/dashboard/plugin/dashboard.plugin.ts` - Add search resolver and scheduled tasks
+- `packages/dashboard/src/lib/components/layout/app-layout.tsx` - Add search trigger
+- `packages/dashboard/plugin/api/api-extensions.ts` - Include search schema
+- `packages/dashboard/src/lib/framework/extension-api/` - Add quick actions to extension API
+
+### Database Migration
+
+```sql
+-- Migration for search index table (PostgreSQL example)
+CREATE TABLE search_index (
+    id VARCHAR(255) PRIMARY KEY,
+    type VARCHAR(50) NOT NULL,
+    title VARCHAR(500) NOT NULL,
+    subtitle VARCHAR(500),
+    description TEXT,
+    content TEXT NOT NULL,
+    url VARCHAR(1000) NOT NULL,
+    thumbnail_url VARCHAR(1000),
+    metadata JSONB,
+    permissions VARCHAR(100)[] DEFAULT '{}',
+    channel_ids VARCHAR(36)[] DEFAULT '{}',
+    entity_id VARCHAR(36),
+    last_updated TIMESTAMP NOT NULL DEFAULT NOW(),
+    search_vector tsvector,
+    relevance_score FLOAT DEFAULT 0
+);
+
+-- Indexes for performance
+CREATE INDEX idx_search_index_type ON search_index(type);
+CREATE INDEX idx_search_index_permissions ON search_index USING GIN(permissions);
+CREATE INDEX idx_search_index_channels ON search_index USING GIN(channel_ids);
+CREATE INDEX idx_search_index_updated ON search_index(last_updated);
+CREATE INDEX idx_search_index_fts ON search_index USING GIN(search_vector);
+
+-- Trigger to automatically update search_vector
+CREATE OR REPLACE FUNCTION update_search_vector()
+RETURNS TRIGGER AS $$
+BEGIN
+    NEW.search_vector := to_tsvector('english',
+        COALESCE(NEW.title, '') || ' ' ||
+        COALESCE(NEW.subtitle, '') || ' ' ||
+        COALESCE(NEW.description, '') || ' ' ||
+        COALESCE(NEW.content, '')
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER search_vector_update
+    BEFORE INSERT OR UPDATE ON search_index
+    FOR EACH ROW EXECUTE FUNCTION update_search_vector();
+```
+
+## Configuration Examples
+
+### Basic Database Search (Default)
+
+```typescript
+// In vendure-config.ts
+import { DashboardPlugin } from '@vendure/dashboard/plugin';
+import {
+    searchFullReindexTask,
+    searchIncrementalUpdateTask,
+    searchExternalContentRefreshTask,
+} from '@vendure/dashboard/plugin/tasks/search-index-task';
+
+const config: VendureConfig = {
+    plugins: [
+        DashboardPlugin.init({
+            route: 'dashboard',
+            appDir: './dist/dashboard',
+            search: {
+                strategy: 'database',
+                indexing: {
+                    fullReindexCron: '0 2 * * *', // Daily at 2 AM
+                    incrementalUpdateCron: '*/15 * * * *', // Every 15 minutes
+                },
+                externalSources: {
+                    documentation: {
+                        enabled: true,
+                        endpoint: 'https://docs.vendure.io/api/search',
+                        refreshIntervalHours: 24,
+                    },
+                    website: {
+                        enabled: true,
+                        endpoint: 'https://vendure.io/api/search',
+                        refreshIntervalHours: 12,
+                    },
+                },
+            },
+        }),
+    ],
+    scheduledTasks: [searchFullReindexTask, searchIncrementalUpdateTask],
+};
+```
+
+### TypeSense Search Configuration
+
+```typescript
+const config: VendureConfig = {
+    plugins: [
+        DashboardPlugin.init({
+            route: 'dashboard',
+            appDir: './dist/dashboard',
+            search: {
+                strategy: 'typesense',
+                strategyOptions: {
+                    typesense: {
+                        nodes: [
+                            {
+                                host: 'localhost',
+                                port: 8108,
+                                protocol: 'http',
+                            },
+                        ],
+                        apiKey: 'xyz123',
+                        collectionName: 'vendure_search',
+                    },
+                },
+                // ... other config
+            },
+        }),
+    ],
+};
+```
+
+## Custom Actions Integration via defineDashboardExtensions
+
+### Extension API Integration
+
+```typescript
+// Extension API for registering custom quick actions
+interface QuickActionExtension {
+    // Global actions (available everywhere)
+    globalActions?: QuickActionDefinition[];
+
+    // Context-aware actions (route/page specific)
+    contextActions?: {
+        [routePattern: string]: QuickActionDefinition[];
+    };
+
+    // Entity-specific actions (based on entity type)
+    entityActions?: {
+        [entityType: string]: QuickActionDefinition[];
+    };
+}
+
+interface QuickActionDefinition {
+    id: string;
+    label: string;
+    description?: string;
+    icon?: string;
+    shortcut?: string;
+    requiredPermissions?: string[];
+    handler: QuickActionHandler;
+    params?: Record<string, any>;
+}
+
+type QuickActionHandler = (context: QuickActionContext) => void | Promise<void>;
+
+interface QuickActionContext {
+    // Current page context
+    currentRoute: string;
+    currentEntityType?: string;
+    currentEntityId?: string;
+
+    // Available utilities
+    navigate: (path: string) => void;
+    showNotification: (message: string, type?: 'success' | 'error' | 'warning') => void;
+    confirm: (message: string) => Promise<boolean>;
+
+    // Data access (if needed)
+    executeGraphQL: (query: string, variables?: any) => Promise<any>;
+}
+```
+
+### Developer Usage Examples
+
+#### Global Custom Action
+
+```typescript
+// In a plugin or custom extension
+defineDashboardExtensions({
+    globalActions: [
+        {
+            id: 'export-all-orders',
+            label: 'Export All Orders',
+            description: 'Export all orders to CSV',
+            icon: 'download',
+            shortcut: 'ctrl+shift+e',
+            requiredPermissions: ['ReadOrder'],
+            handler: async context => {
+                const confirmed = await context.confirm('Export all orders to CSV?');
+                if (confirmed) {
+                    // Implementation here
+                    context.showNotification('Export started', 'success');
+                }
+            },
+        },
+    ],
+});
+```
+
+#### Context-Aware Custom Action
+
+```typescript
+defineDashboardExtensions({
+    contextActions: {
+        // Only show on product detail pages
+        '/products/:id': [
+            {
+                id: 'sync-inventory',
+                label: 'Sync Inventory',
+                description: 'Sync product inventory with external system',
+                icon: 'refresh',
+                shortcut: 'ctrl+shift+s',
+                handler: async context => {
+                    if (context.currentEntityId) {
+                        // Sync specific product
+                        await syncProductInventory(context.currentEntityId);
+                        context.showNotification('Inventory synced', 'success');
+                    }
+                },
+            },
+        ],
+
+        // Show on any list page
+        '/**/list': [
+            {
+                id: 'bulk-export',
+                label: 'Bulk Export',
+                icon: 'download',
+                handler: context => {
+                    context.navigate('/export?type=' + context.currentEntityType);
+                },
+            },
+        ],
+    },
+});
+```
+
+### Built-in Action Examples
+
+#### Global Built-in Actions
+
+- **Create New Product** - `ctrl+shift+p` - Navigate to product creation
+- **Create New Order** - `ctrl+shift+o` - Navigate to order creation
+- **Create New Customer** - `ctrl+shift+c` - Navigate to customer creation
+- **Go to Profile** - `ctrl+shift+u` - Navigate to user profile
+- **Quick Open Order** - `ctrl+o` - Show order search dialog
+
+#### Context-Aware Built-in Actions
+
+- **Product Detail Page:**
+    - **Save Product** - `ctrl+s` - Save current product
+    - **Duplicate Product** - `ctrl+d` - Duplicate current product
+    - **Add Variant** - `ctrl+shift+v` - Add new product variant
+
+- **Order Detail Page:**
+    - **Fulfill Order** - `ctrl+f` - Open fulfillment dialog
+    - **Cancel Order** - `ctrl+shift+x` - Cancel order with confirmation
+    - **Add Payment** - `ctrl+p` - Add payment to order
+
+- **Any List Page:**
+    - **Export Data** - `ctrl+e` - Export current filtered data
+    - **Bulk Edit** - `ctrl+b` - Enter bulk edit mode
+    - **New Item** - `ctrl+n` - Create new item of current type
+
+## Next Steps
+
+1. Review and refine this implementation plan
+2. Define specific search result formats for each entity type
+3. Create detailed wireframes for the search interface
+4. Set up development environment and project structure
+5. Begin Phase 1 implementation
+
+---
+
+_This document is a living plan and should be updated as the implementation progresses and requirements evolve._
+
+```
+
+```