@memberjunction/ng-search 5.24.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/__tests__/search-types.test.js +6 -2
  2. package/dist/__tests__/search-types.test.js.map +1 -1
  3. package/dist/__tests__/search.service.test.js +25 -18
  4. package/dist/__tests__/search.service.test.js.map +1 -1
  5. package/dist/lib/module.d.ts +7 -4
  6. package/dist/lib/module.d.ts.map +1 -1
  7. package/dist/lib/module.js +19 -4
  8. package/dist/lib/module.js.map +1 -1
  9. package/dist/lib/search-composite.component.d.ts +79 -0
  10. package/dist/lib/search-composite.component.d.ts.map +1 -0
  11. package/dist/lib/search-composite.component.js +258 -0
  12. package/dist/lib/search-composite.component.js.map +1 -0
  13. package/dist/lib/search-filter.component.d.ts +14 -1
  14. package/dist/lib/search-filter.component.d.ts.map +1 -1
  15. package/dist/lib/search-filter.component.js +128 -26
  16. package/dist/lib/search-filter.component.js.map +1 -1
  17. package/dist/lib/search-input.component.d.ts +61 -0
  18. package/dist/lib/search-input.component.d.ts.map +1 -0
  19. package/dist/lib/search-input.component.js +192 -0
  20. package/dist/lib/search-input.component.js.map +1 -0
  21. package/dist/lib/search-overlay.component.js +2 -2
  22. package/dist/lib/search-results.component.d.ts +9 -5
  23. package/dist/lib/search-results.component.d.ts.map +1 -1
  24. package/dist/lib/search-results.component.js +117 -98
  25. package/dist/lib/search-results.component.js.map +1 -1
  26. package/dist/lib/search-suggest.component.d.ts +95 -0
  27. package/dist/lib/search-suggest.component.d.ts.map +1 -0
  28. package/dist/lib/search-suggest.component.js +491 -0
  29. package/dist/lib/search-suggest.component.js.map +1 -0
  30. package/dist/lib/search-types.d.ts +27 -1
  31. package/dist/lib/search-types.d.ts.map +1 -1
  32. package/dist/lib/search-types.js.map +1 -1
  33. package/dist/lib/search.service.d.ts +27 -5
  34. package/dist/lib/search.service.d.ts.map +1 -1
  35. package/dist/lib/search.service.js +221 -92
  36. package/dist/lib/search.service.js.map +1 -1
  37. package/dist/public-api.d.ts +3 -0
  38. package/dist/public-api.d.ts.map +1 -1
  39. package/dist/public-api.js +3 -0
  40. package/dist/public-api.js.map +1 -1
  41. package/package.json +6 -4
@@ -18,6 +18,8 @@ export interface SearchResultItem {
18
18
  RecordID: string;
19
19
  /** Classification of the result source */
20
20
  SourceType: 'entity' | 'content-item' | 'file' | 'web-page';
21
+ /** Discriminator for UI rendering: 'entity-record', 'storage-file', or 'content-item' */
22
+ ResultType?: string;
21
23
  /** Relevance score between 0 and 1 */
22
24
  Score: number;
23
25
  /** Breakdown of how the score was computed */
@@ -34,6 +36,12 @@ export interface SearchResultItem {
34
36
  MatchedAt: Date;
35
37
  /** Raw vector metadata JSON — contains all entity fields stored in the vector DB */
36
38
  RawMetadata?: string;
39
+ /** ID of the SearchProvider metadata record that produced this result */
40
+ ProviderId?: string;
41
+ /** Display label from the SearchProvider metadata (e.g., "Database", "Semantic Search") */
42
+ ProviderLabel?: string;
43
+ /** Font Awesome icon class from the SearchProvider metadata */
44
+ ProviderIcon?: string;
37
45
  }
38
46
  /** Breakdown of score contribution from each search source */
39
47
  export interface ScoreBreakdown {
@@ -85,7 +93,7 @@ export interface SearchRequest {
85
93
  /** Active filter selections */
86
94
  ActiveFilters: Record<string, string[]>;
87
95
  /** Which search sources to include */
88
- IncludeSources: ('vector' | 'fulltext' | 'entity')[];
96
+ IncludeSources: ('vector' | 'fulltext' | 'entity' | 'storage')[];
89
97
  /**
90
98
  * Minimum relevance score (0-1) to include in results.
91
99
  * Results below this threshold are filtered out.
@@ -93,6 +101,21 @@ export interface SearchRequest {
93
101
  */
94
102
  MinScore?: number;
95
103
  }
104
+ /** Metadata about an active search provider from the server */
105
+ export interface SearchProviderInfo {
106
+ /** SearchProvider record ID */
107
+ ID: string;
108
+ /** Provider name */
109
+ Name: string;
110
+ /** UI display label */
111
+ DisplayName: string;
112
+ /** Font Awesome icon class */
113
+ Icon: string;
114
+ /** The SourceType key this provider uses */
115
+ SourceType: string;
116
+ /** Priority (lower = higher) */
117
+ Priority: number;
118
+ }
96
119
  /** Search response from the service */
97
120
  export interface SearchResponse {
98
121
  /** Whether the search succeeded */
@@ -112,7 +135,10 @@ export interface SearchResponse {
112
135
  Vector: number;
113
136
  FullText: number;
114
137
  Entity: number;
138
+ Storage: number;
115
139
  };
140
+ /** Active search providers metadata (for UI labels and icons) */
141
+ Providers: SearchProviderInfo[];
116
142
  /** Error message if Success is false */
117
143
  ErrorMessage?: string;
118
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search-types.d.ts","sourceRoot":"","sources":["../../src/lib/search-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"search-types.d.ts","sourceRoot":"","sources":["../../src/lib/search-types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"search-types.js","sourceRoot":"","sources":["../../src/lib/search-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @fileoverview Shared types for the ng-search package.\n *\n * Defines the data contracts for search results, filters,\n * and events used across all search components.\n */\n\n/** Represents a single search result item */\nexport interface SearchResultItem {\n /** Unique identifier */\n ID: string;\n /** Display title */\n Title: string;\n /** Short text snippet showing match context */\n Snippet: string;\n /** The entity name this result belongs to */\n EntityName: string;\n /** The primary key of the source record */\n RecordID: string;\n /** Classification of the result source */\n SourceType: 'entity' | 'content-item' | 'file' | 'web-page';\n /** Relevance score between 0 and 1 */\n Score: number;\n /** Breakdown of how the score was computed */\n ScoreBreakdown: ScoreBreakdown;\n /** Human-readable tags */\n Tags: string[];\n /** Font Awesome icon class for the source type */\n SourceIcon: string;\n /** Entity-specific icon from EntityInfo.Icon (e.g., \"fa-solid fa-robot\") */\n EntityIcon?: string;\n /** Human-readable record name resolved from EntityRecordName */\n RecordName?: string;\n /** When this result was matched */\n MatchedAt: Date;\n /** Raw vector metadata JSON — contains all entity fields stored in the vector DB */\n RawMetadata?: string;\n}\n\n/** Breakdown of score contribution from each search source */\nexport interface ScoreBreakdown {\n Vector?: number;\n FullText?: number;\n Entity?: number;\n}\n\n/** Grouped search results by source type */\nexport interface SearchResultGroup {\n /** The source type label */\n Label: string;\n /** Font Awesome icon class */\n Icon: string;\n /** The source type key */\n SourceType: string;\n /** Results in this group */\n Results: SearchResultItem[];\n /** Total count for this source (may exceed Results.length if paginated) */\n TotalCount: number;\n}\n\n/** Faceted filter configuration */\nexport interface SearchFilter {\n /** Filter category name */\n Category: string;\n /** Filter options within this category */\n Options: SearchFilterOption[];\n /** Whether multiple options can be selected */\n MultiSelect: boolean;\n}\n\n/** Single filter option within a category */\nexport interface SearchFilterOption {\n /** Display label */\n Label: string;\n /** Value to filter by */\n Value: string;\n /** Count of results matching this option */\n Count: number;\n /** Whether this option is currently selected */\n IsSelected: boolean;\n /** Font Awesome icon class */\n Icon?: string;\n}\n\n/** Search request parameters */\nexport interface SearchRequest {\n /** The search query string */\n Query: string;\n /** Maximum number of results to return */\n MaxResults: number;\n /** Active filter selections */\n ActiveFilters: Record<string, string[]>;\n /** Which search sources to include */\n IncludeSources: ('vector' | 'fulltext' | 'entity')[];\n /**\n * Minimum relevance score (0-1) to include in results.\n * Results below this threshold are filtered out.\n * Default: 0.35 (35%).\n */\n MinScore?: number;\n}\n\n/** Search response from the service */\nexport interface SearchResponse {\n /** Whether the search succeeded */\n Success: boolean;\n /** The search results */\n Results: SearchResultItem[];\n /** Grouped results by source type */\n Groups: SearchResultGroup[];\n /** Available filters based on results */\n Filters: SearchFilter[];\n /** Total result count across all sources */\n TotalCount: number;\n /** Time taken in milliseconds */\n ElapsedMs: number;\n /** Count of results from each source */\n SourceCounts: { Vector: number; FullText: number; Entity: number };\n /** Error message if Success is false */\n ErrorMessage?: string;\n}\n\n/** Event emitted when a search result is selected */\nexport interface SearchResultSelectedEvent {\n /** The selected result item */\n Result: SearchResultItem;\n /** Whether to open in a new tab */\n OpenInNewTab: boolean;\n}\n\n/** Event emitted when filters change */\nexport interface SearchFilterChangeEvent {\n /** The filter category that changed */\n Category: string;\n /** The new selected values */\n SelectedValues: string[];\n}\n\n/** Event emitted when search is executed */\nexport interface SearchExecutedEvent {\n /** The search query */\n Query: string;\n /** Active filters */\n Filters: Record<string, string[]>;\n /** Number of results returned */\n ResultCount: number;\n /** Time taken in ms */\n ElapsedMs: number;\n}\n"]}
1
+ {"version":3,"file":"search-types.js","sourceRoot":"","sources":["../../src/lib/search-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @fileoverview Shared types for the ng-search package.\n *\n * Defines the data contracts for search results, filters,\n * and events used across all search components.\n */\n\n/** Represents a single search result item */\nexport interface SearchResultItem {\n /** Unique identifier */\n ID: string;\n /** Display title */\n Title: string;\n /** Short text snippet showing match context */\n Snippet: string;\n /** The entity name this result belongs to */\n EntityName: string;\n /** The primary key of the source record */\n RecordID: string;\n /** Classification of the result source */\n SourceType: 'entity' | 'content-item' | 'file' | 'web-page';\n /** Discriminator for UI rendering: 'entity-record', 'storage-file', or 'content-item' */\n ResultType?: string;\n /** Relevance score between 0 and 1 */\n Score: number;\n /** Breakdown of how the score was computed */\n ScoreBreakdown: ScoreBreakdown;\n /** Human-readable tags */\n Tags: string[];\n /** Font Awesome icon class for the source type */\n SourceIcon: string;\n /** Entity-specific icon from EntityInfo.Icon (e.g., \"fa-solid fa-robot\") */\n EntityIcon?: string;\n /** Human-readable record name resolved from EntityRecordName */\n RecordName?: string;\n /** When this result was matched */\n MatchedAt: Date;\n /** Raw vector metadata JSON — contains all entity fields stored in the vector DB */\n RawMetadata?: string;\n /** ID of the SearchProvider metadata record that produced this result */\n ProviderId?: string;\n /** Display label from the SearchProvider metadata (e.g., \"Database\", \"Semantic Search\") */\n ProviderLabel?: string;\n /** Font Awesome icon class from the SearchProvider metadata */\n ProviderIcon?: string;\n}\n\n/** Breakdown of score contribution from each search source */\nexport interface ScoreBreakdown {\n Vector?: number;\n FullText?: number;\n Entity?: number;\n}\n\n/** Grouped search results by source type */\nexport interface SearchResultGroup {\n /** The source type label */\n Label: string;\n /** Font Awesome icon class */\n Icon: string;\n /** The source type key */\n SourceType: string;\n /** Results in this group */\n Results: SearchResultItem[];\n /** Total count for this source (may exceed Results.length if paginated) */\n TotalCount: number;\n}\n\n/** Faceted filter configuration */\nexport interface SearchFilter {\n /** Filter category name */\n Category: string;\n /** Filter options within this category */\n Options: SearchFilterOption[];\n /** Whether multiple options can be selected */\n MultiSelect: boolean;\n}\n\n/** Single filter option within a category */\nexport interface SearchFilterOption {\n /** Display label */\n Label: string;\n /** Value to filter by */\n Value: string;\n /** Count of results matching this option */\n Count: number;\n /** Whether this option is currently selected */\n IsSelected: boolean;\n /** Font Awesome icon class */\n Icon?: string;\n}\n\n/** Search request parameters */\nexport interface SearchRequest {\n /** The search query string */\n Query: string;\n /** Maximum number of results to return */\n MaxResults: number;\n /** Active filter selections */\n ActiveFilters: Record<string, string[]>;\n /** Which search sources to include */\n IncludeSources: ('vector' | 'fulltext' | 'entity' | 'storage')[];\n /**\n * Minimum relevance score (0-1) to include in results.\n * Results below this threshold are filtered out.\n * Default: 0.35 (35%).\n */\n MinScore?: number;\n}\n\n/** Metadata about an active search provider from the server */\nexport interface SearchProviderInfo {\n /** SearchProvider record ID */\n ID: string;\n /** Provider name */\n Name: string;\n /** UI display label */\n DisplayName: string;\n /** Font Awesome icon class */\n Icon: string;\n /** The SourceType key this provider uses */\n SourceType: string;\n /** Priority (lower = higher) */\n Priority: number;\n}\n\n/** Search response from the service */\nexport interface SearchResponse {\n /** Whether the search succeeded */\n Success: boolean;\n /** The search results */\n Results: SearchResultItem[];\n /** Grouped results by source type */\n Groups: SearchResultGroup[];\n /** Available filters based on results */\n Filters: SearchFilter[];\n /** Total result count across all sources */\n TotalCount: number;\n /** Time taken in milliseconds */\n ElapsedMs: number;\n /** Count of results from each source */\n SourceCounts: { Vector: number; FullText: number; Entity: number; Storage: number };\n /** Active search providers metadata (for UI labels and icons) */\n Providers: SearchProviderInfo[];\n /** Error message if Success is false */\n ErrorMessage?: string;\n}\n\n/** Event emitted when a search result is selected */\nexport interface SearchResultSelectedEvent {\n /** The selected result item */\n Result: SearchResultItem;\n /** Whether to open in a new tab */\n OpenInNewTab: boolean;\n}\n\n/** Event emitted when filters change */\nexport interface SearchFilterChangeEvent {\n /** The filter category that changed */\n Category: string;\n /** The new selected values */\n SelectedValues: string[];\n}\n\n/** Event emitted when search is executed */\nexport interface SearchExecutedEvent {\n /** The search query */\n Query: string;\n /** Active filters */\n Filters: Record<string, string[]>;\n /** Number of results returned */\n ResultCount: number;\n /** Time taken in ms */\n ElapsedMs: number;\n}\n"]}
@@ -12,18 +12,27 @@ export declare class SearchService {
12
12
  SearchResults$: BehaviorSubject<SearchResponse | null>;
13
13
  /** Whether a search is in progress */
14
14
  IsSearching$: BehaviorSubject<boolean>;
15
- /** Recent search history (kept in memory, max 20 entries) */
15
+ /** Recent search history (persisted via UserInfoEngine, max 20 entries) */
16
16
  RecentSearches$: BehaviorSubject<RecentSearch[]>;
17
17
  /** Error stream */
18
18
  Errors$: Subject<string>;
19
19
  private readonly maxRecentSearches;
20
+ private recentSearchesLoaded;
21
+ /** Cached provider metadata keyed by SourceType, populated from search responses */
22
+ private providersBySourceType;
20
23
  /**
21
24
  * Execute a search request via the SearchKnowledge GraphQL mutation.
22
25
  */
23
26
  ExecuteSearch(request: SearchRequest): Promise<SearchResponse>;
27
+ /**
28
+ * Lightweight preview search for autocomplete/typeahead.
29
+ * Does NOT update SearchResults$ or IsSearching$ observables,
30
+ * and does NOT add to recent search history.
31
+ */
32
+ PreviewSearch(query: string, maxResults?: number): Promise<SearchResponse>;
24
33
  /** Clear the current search results */
25
34
  ClearResults(): void;
26
- /** Clear recent search history */
35
+ /** Clear recent search history (memory and persisted) */
27
36
  ClearRecentSearches(): void;
28
37
  /**
29
38
  * Group flat results by source type for display.
@@ -37,15 +46,28 @@ export declare class SearchService {
37
46
  GetSourceIcon(sourceType: string): string;
38
47
  private buildSourceTypeFilter;
39
48
  private buildEntityNameFilter;
49
+ private buildFileTypeFilter;
40
50
  private buildTagFilter;
51
+ /**
52
+ * Load persisted recent searches from UserInfoEngine.
53
+ * Safe to call multiple times — only loads once.
54
+ */
55
+ LoadRecentSearches(): Promise<void>;
41
56
  private addToRecentSearches;
57
+ private persistRecentSearches;
58
+ /** Cache provider metadata by SourceType for use in GroupResults/BuildFilters */
59
+ private cacheProviders;
42
60
  private createEmptyResponse;
43
61
  /**
44
- * Execute search via the SearchKnowledge GraphQL mutation.
62
+ * Execute search via the GraphQLSearchClient.
45
63
  */
46
64
  private executeGraphQLSearch;
47
- /** Build the GraphQL filters input from active filter selections */
48
- private buildFiltersInput;
65
+ /** Convert a SearchClientResponse into the SearchResponse shape used by this service */
66
+ private mapClientResponseToSearchResponse;
67
+ /** Map a single SearchClientResultItem to a SearchResultItem */
68
+ private mapClientResultItem;
69
+ /** Build the client filters from active filter selections */
70
+ private buildClientFilters;
49
71
  static ɵfac: i0.ɵɵFactoryDeclaration<SearchService, never>;
50
72
  static ɵprov: i0.ɵɵInjectableDeclaration<SearchService>;
51
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.service.d.ts","sourceRoot":"","sources":["../../src/lib/search.service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEhD,OAAO,EACH,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EAEf,MAAM,gBAAgB,CAAC;;AAqBxB,sCAAsC;AACtC,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,qBACa,aAAa;IACtB,8BAA8B;IACvB,cAAc,yCAAoD;IAEzE,sCAAsC;IAC/B,YAAY,2BAAuC;IAE1D,6DAA6D;IACtD,eAAe,kCAA2C;IAEjE,mBAAmB;IACZ,OAAO,kBAAyB;IAEvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAM;IAExC;;OAEG;IACU,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAuB3E,uCAAuC;IAChC,YAAY,IAAI,IAAI;IAI3B,kCAAkC;IAC3B,mBAAmB,IAAI,IAAI;IAIlC;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,iBAAiB,EAAE;IAsBrE;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE;IAQhE,2CAA2C;IACpC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIhD,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,mBAAmB;IAa3B;;OAEG;YACW,oBAAoB;IAwGlC,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;yCAnRhB,aAAa;6CAAb,aAAa;CA2SzB"}
1
+ {"version":3,"file":"search.service.d.ts","sourceRoot":"","sources":["../../src/lib/search.service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAUhD,OAAO,EACH,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EAGf,MAAM,gBAAgB,CAAC;;AA8BxB,sCAAsC;AACtC,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB;AASD,qBACa,aAAa;IACtB,8BAA8B;IACvB,cAAc,yCAAoD;IAEzE,sCAAsC;IAC/B,YAAY,2BAAuC;IAE1D,2EAA2E;IACpE,eAAe,kCAA2C;IAEjE,mBAAmB;IACZ,OAAO,kBAAyB;IAEvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAM;IACxC,OAAO,CAAC,oBAAoB,CAAS;IAErC,oFAAoF;IACpF,OAAO,CAAC,qBAAqB,CAAyC;IAEtE;;OAEG;IACU,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAuB3E;;;;OAIG;IACU,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,GAAE,MAAU,GAAG,OAAO,CAAC,cAAc,CAAC;IAoB1F,uCAAuC;IAChC,YAAY,IAAI,IAAI;IAI3B,yDAAyD;IAClD,mBAAmB,IAAI,IAAI;IAKlC;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,iBAAiB,EAAE;IA0BrE;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE;IAShE,2CAA2C;IACpC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIhD,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,qBAAqB;IAkC7B,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,cAAc;IAoBtB;;;OAGG;IACU,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BhD,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,qBAAqB;IAa7B,iFAAiF;IACjF,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,mBAAmB;IAc3B;;OAEG;YACW,oBAAoB;IAwBlC,wFAAwF;IACxF,OAAO,CAAC,iCAAiC;IAsCzC,gEAAgE;IAChE,OAAO,CAAC,mBAAmB;IAuB3B,6DAA6D;IAC7D,OAAO,CAAC,kBAAkB;yCAvYjB,aAAa;6CAAb,aAAa;CA+ZzB"}
@@ -6,34 +6,48 @@
6
6
  */
7
7
  import { Injectable } from '@angular/core';
8
8
  import { BehaviorSubject, Subject } from 'rxjs';
9
- import { Metadata } from '@memberjunction/core';
9
+ import { Metadata, StartupManager } from '@memberjunction/core';
10
+ import { UserInfoEngine } from '@memberjunction/core-entities';
11
+ import { MJEventType, MJGlobal } from '@memberjunction/global';
12
+ import { GraphQLSearchClient, GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
10
13
  import * as i0 from "@angular/core";
11
14
  /** Default minimum relevance score threshold (0-1). Results below this are filtered out. */
12
15
  const DEFAULT_MIN_SCORE = 0.35;
13
- /** Default icon mapping for source types */
14
- const SOURCE_TYPE_ICONS = {
16
+ /** Fallback icon mapping for source types (used when provider metadata is not available) */
17
+ const FALLBACK_SOURCE_ICONS = {
15
18
  'entity': 'fa-solid fa-database',
19
+ 'vector': 'fa-solid fa-brain',
20
+ 'fulltext': 'fa-solid fa-magnifying-glass',
16
21
  'content-item': 'fa-solid fa-file-lines',
17
22
  'file': 'fa-solid fa-file',
18
23
  'web-page': 'fa-solid fa-globe',
24
+ 'storage': 'fa-solid fa-folder-open',
19
25
  };
20
- /** Default labels for source types */
21
- const SOURCE_TYPE_LABELS = {
22
- 'entity': 'Entity Records',
26
+ /** Fallback labels for source types (used when provider metadata is not available) */
27
+ const FALLBACK_SOURCE_LABELS = {
28
+ 'entity': 'Database',
29
+ 'vector': 'Semantic Search',
30
+ 'fulltext': 'Full-Text Search',
23
31
  'content-item': 'Content Items',
24
32
  'file': 'Documents',
25
33
  'web-page': 'Web Pages',
34
+ 'storage': 'File Storage',
26
35
  };
36
+ /** User setting key for persisting recent searches */
37
+ const RECENT_SEARCHES_KEY = 'search.recentSearches';
27
38
  export class SearchService {
28
39
  /** Current search response */
29
40
  SearchResults$ = new BehaviorSubject(null);
30
41
  /** Whether a search is in progress */
31
42
  IsSearching$ = new BehaviorSubject(false);
32
- /** Recent search history (kept in memory, max 20 entries) */
43
+ /** Recent search history (persisted via UserInfoEngine, max 20 entries) */
33
44
  RecentSearches$ = new BehaviorSubject([]);
34
45
  /** Error stream */
35
46
  Errors$ = new Subject();
36
47
  maxRecentSearches = 20;
48
+ recentSearchesLoaded = false;
49
+ /** Cached provider metadata keyed by SourceType, populated from search responses */
50
+ providersBySourceType = new Map();
37
51
  /**
38
52
  * Execute a search request via the SearchKnowledge GraphQL mutation.
39
53
  */
@@ -58,13 +72,37 @@ export class SearchService {
58
72
  this.IsSearching$.next(false);
59
73
  }
60
74
  }
75
+ /**
76
+ * Lightweight preview search for autocomplete/typeahead.
77
+ * Does NOT update SearchResults$ or IsSearching$ observables,
78
+ * and does NOT add to recent search history.
79
+ */
80
+ async PreviewSearch(query, maxResults = 8) {
81
+ try {
82
+ if (!query.trim()) {
83
+ return this.createEmptyResponse();
84
+ }
85
+ const provider = Metadata.Provider;
86
+ if (!(provider instanceof GraphQLDataProvider)) {
87
+ return this.createEmptyResponse('GraphQL provider not available');
88
+ }
89
+ const client = new GraphQLSearchClient(provider);
90
+ const clientResponse = await client.PreviewSearch(query, maxResults);
91
+ return this.mapClientResponseToSearchResponse(clientResponse);
92
+ }
93
+ catch (err) {
94
+ const errorMessage = err instanceof Error ? err.message : 'Preview search failed';
95
+ return this.createEmptyResponse(errorMessage);
96
+ }
97
+ }
61
98
  /** Clear the current search results */
62
99
  ClearResults() {
63
100
  this.SearchResults$.next(null);
64
101
  }
65
- /** Clear recent search history */
102
+ /** Clear recent search history (memory and persisted) */
66
103
  ClearRecentSearches() {
67
104
  this.RecentSearches$.next([]);
105
+ this.persistRecentSearches([]);
68
106
  }
69
107
  /**
70
108
  * Group flat results by source type for display.
@@ -81,13 +119,17 @@ export class SearchService {
81
119
  groupMap.set(key, [result]);
82
120
  }
83
121
  }
84
- return Array.from(groupMap.entries()).map(([sourceType, items]) => ({
85
- Label: SOURCE_TYPE_LABELS[sourceType] ?? sourceType,
86
- Icon: SOURCE_TYPE_ICONS[sourceType] ?? 'fa-solid fa-circle',
87
- SourceType: sourceType,
88
- Results: items,
89
- TotalCount: items.length
90
- }));
122
+ return Array.from(groupMap.entries()).map(([sourceType, items]) => {
123
+ // Priority: cached provider metadata > per-result metadata > fallback maps
124
+ const cached = this.providersBySourceType.get(sourceType);
125
+ return {
126
+ Label: cached?.DisplayName ?? items[0]?.ProviderLabel ?? FALLBACK_SOURCE_LABELS[sourceType] ?? sourceType,
127
+ Icon: cached?.Icon ?? items[0]?.ProviderIcon ?? FALLBACK_SOURCE_ICONS[sourceType] ?? 'fa-solid fa-circle',
128
+ SourceType: sourceType,
129
+ Results: items,
130
+ TotalCount: items.length
131
+ };
132
+ });
91
133
  }
92
134
  /**
93
135
  * Build filter facets from a set of results.
@@ -96,31 +138,41 @@ export class SearchService {
96
138
  return [
97
139
  this.buildSourceTypeFilter(results),
98
140
  this.buildEntityNameFilter(results),
141
+ this.buildFileTypeFilter(results),
99
142
  this.buildTagFilter(results)
100
- ];
143
+ ].filter(f => f.Options.length > 0); // Hide categories with no options
101
144
  }
102
145
  /** Get the icon for a given source type */
103
146
  GetSourceIcon(sourceType) {
104
- return SOURCE_TYPE_ICONS[sourceType] ?? 'fa-solid fa-circle';
147
+ return this.providersBySourceType.get(sourceType)?.Icon ?? FALLBACK_SOURCE_ICONS[sourceType] ?? 'fa-solid fa-circle';
105
148
  }
106
149
  buildSourceTypeFilter(results) {
107
150
  const counts = new Map();
108
151
  for (const r of results) {
109
152
  counts.set(r.SourceType, (counts.get(r.SourceType) ?? 0) + 1);
110
153
  }
111
- const options = Array.from(counts.entries()).map(([type, count]) => ({
112
- Label: SOURCE_TYPE_LABELS[type] ?? type,
113
- Value: type,
114
- Count: count,
115
- IsSelected: false,
116
- Icon: SOURCE_TYPE_ICONS[type]
117
- }));
118
- return { Category: 'Source Type', Options: options, MultiSelect: true };
154
+ const options = Array.from(counts.entries()).map(([type, count]) => {
155
+ const cached = this.providersBySourceType.get(type);
156
+ return {
157
+ Label: cached?.DisplayName ?? FALLBACK_SOURCE_LABELS[type] ?? type,
158
+ Value: type,
159
+ Count: count,
160
+ IsSelected: false,
161
+ Icon: cached?.Icon ?? FALLBACK_SOURCE_ICONS[type]
162
+ };
163
+ });
164
+ return { Category: 'Source', Options: options, MultiSelect: true };
119
165
  }
120
166
  buildEntityNameFilter(results) {
121
167
  const counts = new Map();
168
+ let fileCount = 0;
122
169
  for (const r of results) {
123
- counts.set(r.EntityName, (counts.get(r.EntityName) ?? 0) + 1);
170
+ if (r.ResultType === 'storage-file') {
171
+ fileCount++;
172
+ }
173
+ else {
174
+ counts.set(r.EntityName, (counts.get(r.EntityName) ?? 0) + 1);
175
+ }
124
176
  }
125
177
  const options = Array.from(counts.entries())
126
178
  .sort((a, b) => b[1] - a[1])
@@ -131,8 +183,39 @@ export class SearchService {
131
183
  IsSelected: false,
132
184
  Icon: 'fa-solid fa-table'
133
185
  }));
186
+ // Add a single "Files" entry for all storage file results
187
+ if (fileCount > 0) {
188
+ options.push({
189
+ Label: 'Files',
190
+ Value: '__storage-files__',
191
+ Count: fileCount,
192
+ IsSelected: false,
193
+ Icon: 'fa-solid fa-file'
194
+ });
195
+ }
134
196
  return { Category: 'Entity', Options: options, MultiSelect: true };
135
197
  }
198
+ buildFileTypeFilter(results) {
199
+ const counts = new Map();
200
+ for (const r of results) {
201
+ if (r.ResultType !== 'storage-file')
202
+ continue;
203
+ const ext = r.Title?.split('.').pop()?.toUpperCase();
204
+ if (ext && ext.length <= 6) {
205
+ counts.set(ext, (counts.get(ext) ?? 0) + 1);
206
+ }
207
+ }
208
+ const options = Array.from(counts.entries())
209
+ .sort((a, b) => b[1] - a[1])
210
+ .map(([ext, count]) => ({
211
+ Label: ext,
212
+ Value: ext,
213
+ Count: count,
214
+ IsSelected: false,
215
+ Icon: 'fa-solid fa-file'
216
+ }));
217
+ return { Category: 'File Type', Options: options, MultiSelect: true };
218
+ }
136
219
  buildTagFilter(results) {
137
220
  const counts = new Map();
138
221
  for (const r of results) {
@@ -152,6 +235,38 @@ export class SearchService {
152
235
  }));
153
236
  return { Category: 'Tags', Options: options, MultiSelect: true };
154
237
  }
238
+ /**
239
+ * Load persisted recent searches from UserInfoEngine.
240
+ * Safe to call multiple times — only loads once.
241
+ */
242
+ async LoadRecentSearches() {
243
+ if (this.recentSearchesLoaded)
244
+ return;
245
+ try {
246
+ MJGlobal.Instance.GetEventListener(true).subscribe(async (event) => {
247
+ if (event.event === MJEventType.LoggedIn) {
248
+ await StartupManager.Instance.Startup();
249
+ const engine = UserInfoEngine.Instance;
250
+ console.log('[SearchService] After Startup — engine.Loaded:', engine.Loaded, 'UserSettings count:', engine.UserSettings.length);
251
+ const json = engine.GetSetting(RECENT_SEARCHES_KEY);
252
+ console.log('[SearchService] GetSetting("' + RECENT_SEARCHES_KEY + '"):', json?.substring(0, 100));
253
+ if (json) {
254
+ const parsed = JSON.parse(json);
255
+ const searches = parsed.map(s => ({
256
+ Query: s.Query,
257
+ Timestamp: new Date(s.Timestamp),
258
+ ResultCount: s.ResultCount
259
+ }));
260
+ this.RecentSearches$.next(searches);
261
+ }
262
+ }
263
+ });
264
+ }
265
+ catch (err) {
266
+ console.error('[SearchService] LoadRecentSearches error:', err);
267
+ }
268
+ this.recentSearchesLoaded = true;
269
+ }
155
270
  addToRecentSearches(query, resultCount) {
156
271
  const current = this.RecentSearches$.value;
157
272
  const filtered = current.filter(s => s.Query !== query);
@@ -160,6 +275,26 @@ export class SearchService {
160
275
  ...filtered
161
276
  ].slice(0, this.maxRecentSearches);
162
277
  this.RecentSearches$.next(updated);
278
+ this.persistRecentSearches(updated);
279
+ }
280
+ persistRecentSearches(searches) {
281
+ try {
282
+ const json = searches.map(s => ({
283
+ Query: s.Query,
284
+ Timestamp: s.Timestamp.toISOString(),
285
+ ResultCount: s.ResultCount
286
+ }));
287
+ UserInfoEngine.Instance.SetSettingDebounced(RECENT_SEARCHES_KEY, JSON.stringify(json));
288
+ }
289
+ catch {
290
+ // Silently fail if UserInfoEngine not available
291
+ }
292
+ }
293
+ /** Cache provider metadata by SourceType for use in GroupResults/BuildFilters */
294
+ cacheProviders(providers) {
295
+ for (const p of providers) {
296
+ this.providersBySourceType.set(p.SourceType, p);
297
+ }
163
298
  }
164
299
  createEmptyResponse(errorMessage) {
165
300
  return {
@@ -169,107 +304,101 @@ export class SearchService {
169
304
  Filters: [],
170
305
  TotalCount: 0,
171
306
  ElapsedMs: 0,
172
- SourceCounts: { Vector: 0, FullText: 0, Entity: 0 },
307
+ SourceCounts: { Vector: 0, FullText: 0, Entity: 0, Storage: 0 },
308
+ Providers: [],
173
309
  ErrorMessage: errorMessage
174
310
  };
175
311
  }
176
312
  /**
177
- * Execute search via the SearchKnowledge GraphQL mutation.
313
+ * Execute search via the GraphQLSearchClient.
178
314
  */
179
315
  async executeGraphQLSearch(request) {
180
316
  if (!request.Query.trim()) {
181
317
  return this.createEmptyResponse();
182
318
  }
183
319
  const provider = Metadata.Provider;
184
- if (!provider?.ExecuteGQL) {
320
+ if (!(provider instanceof GraphQLDataProvider)) {
185
321
  return this.createEmptyResponse('GraphQL provider not available');
186
322
  }
187
- const mutation = `
188
- mutation SearchKnowledge($query: String!, $maxResults: Float, $filters: SearchFiltersInput, $minScore: Float) {
189
- SearchKnowledge(query: $query, maxResults: $maxResults, filters: $filters, minScore: $minScore) {
190
- Success
191
- TotalCount
192
- ElapsedMs
193
- ErrorMessage
194
- SourceCounts {
195
- Vector
196
- FullText
197
- Entity
198
- }
199
- Results {
200
- ID
201
- EntityName
202
- RecordID
203
- SourceType
204
- Title
205
- Snippet
206
- Score
207
- ScoreBreakdown {
208
- Vector
209
- FullText
210
- Entity
211
- }
212
- Tags
213
- EntityIcon
214
- RecordName
215
- MatchedAt
216
- RawMetadata
217
- }
218
- }
219
- }
220
- `;
221
- const filtersInput = this.buildFiltersInput(request);
323
+ const client = new GraphQLSearchClient(provider);
324
+ const filters = this.buildClientFilters(request);
222
325
  const minScore = request.MinScore ?? DEFAULT_MIN_SCORE;
223
- const variables = {
224
- query: request.Query,
225
- maxResults: request.MaxResults || 20,
226
- filters: filtersInput,
227
- minScore: minScore > 0 ? minScore : undefined,
228
- };
229
- const gqlResult = await provider.ExecuteGQL(mutation, variables);
230
- const data = gqlResult['SearchKnowledge'];
231
- if (!data?.Success) {
232
- return this.createEmptyResponse(data?.ErrorMessage ?? 'Search failed');
326
+ const clientResponse = await client.ExecuteSearch({
327
+ Query: request.Query,
328
+ MaxResults: request.MaxResults || 20,
329
+ MinScore: minScore > 0 ? minScore : undefined,
330
+ Filters: filters
331
+ });
332
+ return this.mapClientResponseToSearchResponse(clientResponse);
333
+ }
334
+ /** Convert a SearchClientResponse into the SearchResponse shape used by this service */
335
+ mapClientResponseToSearchResponse(clientResponse) {
336
+ if (!clientResponse.Success) {
337
+ return this.createEmptyResponse(clientResponse.ErrorMessage ?? 'Search failed');
233
338
  }
234
- const results = (data.Results ?? []).map(r => ({
339
+ // Cache provider metadata for use in GroupResults/BuildFilters
340
+ const providers = (clientResponse.Providers ?? []).map(p => ({
341
+ ID: p.ID,
342
+ Name: p.Name,
343
+ DisplayName: p.DisplayName,
344
+ Icon: p.Icon,
345
+ SourceType: p.SourceType,
346
+ Priority: p.Priority,
347
+ }));
348
+ this.cacheProviders(providers);
349
+ const results = (clientResponse.Results ?? []).map(r => this.mapClientResultItem(r));
350
+ const groups = this.GroupResults(results);
351
+ const filters = this.BuildFilters(results);
352
+ return {
353
+ Success: true,
354
+ Results: results,
355
+ Groups: groups,
356
+ Filters: filters,
357
+ TotalCount: clientResponse.TotalCount,
358
+ ElapsedMs: clientResponse.ElapsedMs,
359
+ SourceCounts: {
360
+ Vector: clientResponse.SourceCounts.Vector,
361
+ FullText: clientResponse.SourceCounts.FullText,
362
+ Entity: clientResponse.SourceCounts.Entity,
363
+ Storage: clientResponse.SourceCounts.Storage
364
+ },
365
+ Providers: providers,
366
+ };
367
+ }
368
+ /** Map a single SearchClientResultItem to a SearchResultItem */
369
+ mapClientResultItem(r) {
370
+ return {
235
371
  ID: r.ID,
236
372
  Title: r.RecordName || r.Title,
237
373
  Snippet: r.Snippet,
238
374
  EntityName: r.EntityName,
239
375
  RecordID: r.RecordID,
240
376
  SourceType: r.SourceType || 'entity',
377
+ ResultType: r.ResultType,
241
378
  Score: r.Score,
242
379
  ScoreBreakdown: r.ScoreBreakdown ?? {},
243
380
  Tags: r.Tags ?? [],
244
- SourceIcon: r.EntityIcon || SOURCE_TYPE_ICONS[r.SourceType] || 'fa-solid fa-database',
381
+ SourceIcon: r.EntityIcon || r.ProviderIcon || FALLBACK_SOURCE_ICONS[r.SourceType] || 'fa-solid fa-database',
245
382
  EntityIcon: r.EntityIcon,
246
383
  RecordName: r.RecordName,
247
384
  MatchedAt: new Date(r.MatchedAt),
248
385
  RawMetadata: r.RawMetadata,
249
- }));
250
- const groups = this.GroupResults(results);
251
- const filters = this.BuildFilters(results);
252
- return {
253
- Success: true,
254
- Results: results,
255
- Groups: groups,
256
- Filters: filters,
257
- TotalCount: data.TotalCount,
258
- ElapsedMs: data.ElapsedMs,
259
- SourceCounts: data.SourceCounts,
386
+ ProviderId: r.ProviderId,
387
+ ProviderLabel: r.ProviderLabel,
388
+ ProviderIcon: r.ProviderIcon,
260
389
  };
261
390
  }
262
- /** Build the GraphQL filters input from active filter selections */
263
- buildFiltersInput(request) {
391
+ /** Build the client filters from active filter selections */
392
+ buildClientFilters(request) {
264
393
  if (!request.ActiveFilters || Object.keys(request.ActiveFilters).length === 0) {
265
- return null;
394
+ return undefined;
266
395
  }
267
396
  const result = {};
268
397
  const entityFilters = request.ActiveFilters['Entity'];
269
398
  if (entityFilters?.length) {
270
399
  result.EntityNames = entityFilters;
271
400
  }
272
- const sourceFilters = request.ActiveFilters['Source Type'];
401
+ const sourceFilters = request.ActiveFilters['Source'];
273
402
  if (sourceFilters?.length) {
274
403
  result.SourceTypes = sourceFilters;
275
404
  }
@@ -277,7 +406,7 @@ export class SearchService {
277
406
  if (tagFilters?.length) {
278
407
  result.Tags = tagFilters;
279
408
  }
280
- return Object.keys(result).length > 0 ? result : null;
409
+ return Object.keys(result).length > 0 ? result : undefined;
281
410
  }
282
411
  static ɵfac = function SearchService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SearchService)(); };
283
412
  static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SearchService, factory: SearchService.ɵfac, providedIn: 'root' });