@danielsimonjr/memory-mcp 0.41.0 → 0.48.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.
- package/dist/__tests__/file-path.test.js +6 -2
- package/dist/__tests__/performance/benchmarks.test.js +151 -149
- package/dist/core/KnowledgeGraphManager.js +43 -20
- package/dist/features/AnalyticsManager.js +11 -11
- package/dist/features/CompressionManager.js +1 -20
- package/dist/features/index.js +1 -1
- package/dist/index.js +4 -37
- package/dist/memory-saved-searches.jsonl +0 -0
- package/dist/memory-tag-aliases.jsonl +0 -0
- package/dist/memory.jsonl +23 -222
- package/dist/search/BasicSearch.js +21 -51
- package/dist/search/BooleanSearch.js +9 -30
- package/dist/search/FuzzySearch.js +11 -30
- package/dist/search/RankedSearch.js +4 -20
- package/dist/search/SearchFilterChain.js +187 -0
- package/dist/search/index.js +4 -0
- package/dist/server/MCPServer.js +5 -842
- package/dist/server/toolDefinitions.js +732 -0
- package/dist/server/toolHandlers.js +117 -0
- package/dist/types/import-export.types.js +1 -1
- package/dist/utils/constants.js +3 -2
- package/dist/utils/entityUtils.js +108 -0
- package/dist/utils/filterUtils.js +155 -0
- package/dist/utils/index.js +26 -0
- package/dist/utils/paginationUtils.js +81 -0
- package/dist/utils/responseFormatter.js +55 -0
- package/dist/utils/tagUtils.js +107 -0
- package/dist/utils/validationHelper.js +99 -0
- package/package.json +34 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { isWithinDateRange } from '../utils/dateUtils.js';
|
|
9
9
|
import { SEARCH_LIMITS } from '../utils/constants.js';
|
|
10
10
|
import { searchCaches } from '../utils/searchCache.js';
|
|
11
|
+
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
11
12
|
/**
|
|
12
13
|
* Performs basic text search with optional filters and caching.
|
|
13
14
|
*/
|
|
@@ -41,37 +42,19 @@ export class BasicSearch {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
const graph = await this.storage.loadGraph();
|
|
44
|
-
const
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const matchesQuery = e.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
51
|
-
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
|
|
52
|
-
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()));
|
|
53
|
-
if (!matchesQuery)
|
|
54
|
-
return false;
|
|
55
|
-
// Tag filter
|
|
56
|
-
if (normalizedTags && normalizedTags.length > 0) {
|
|
57
|
-
if (!e.tags || e.tags.length === 0)
|
|
58
|
-
return false;
|
|
59
|
-
const entityTags = e.tags.map(tag => tag.toLowerCase());
|
|
60
|
-
const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
|
|
61
|
-
if (!hasMatchingTag)
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
// Importance filter
|
|
65
|
-
if (minImportance !== undefined && (e.importance === undefined || e.importance < minImportance)) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
if (maxImportance !== undefined && (e.importance === undefined || e.importance > maxImportance)) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
45
|
+
const queryLower = query.toLowerCase();
|
|
46
|
+
// First filter by text match (search-specific)
|
|
47
|
+
const textMatched = graph.entities.filter(e => {
|
|
48
|
+
return (e.name.toLowerCase().includes(queryLower) ||
|
|
49
|
+
e.entityType.toLowerCase().includes(queryLower) ||
|
|
50
|
+
e.observations.some(o => o.toLowerCase().includes(queryLower)));
|
|
72
51
|
});
|
|
73
|
-
// Apply
|
|
74
|
-
const
|
|
52
|
+
// Apply tag and importance filters using SearchFilterChain
|
|
53
|
+
const filters = { tags, minImportance, maxImportance };
|
|
54
|
+
const filteredEntities = SearchFilterChain.applyFilters(textMatched, filters);
|
|
55
|
+
// Apply pagination using SearchFilterChain
|
|
56
|
+
const pagination = SearchFilterChain.validatePagination(offset, limit);
|
|
57
|
+
const paginatedEntities = SearchFilterChain.paginate(filteredEntities, pagination);
|
|
75
58
|
const filteredEntityNames = new Set(paginatedEntities.map(e => e.name));
|
|
76
59
|
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
77
60
|
const result = { entities: paginatedEntities, relations: filteredRelations };
|
|
@@ -116,33 +99,20 @@ export class BasicSearch {
|
|
|
116
99
|
}
|
|
117
100
|
}
|
|
118
101
|
const graph = await this.storage.loadGraph();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const validatedOffset = Math.max(0, offset);
|
|
122
|
-
const validatedLimit = Math.min(Math.max(SEARCH_LIMITS.MIN, limit), SEARCH_LIMITS.MAX);
|
|
123
|
-
const filteredEntities = graph.entities.filter(e => {
|
|
124
|
-
// Date filter (use createdAt or lastModified)
|
|
102
|
+
// First filter by date range (search-specific - uses createdAt OR lastModified)
|
|
103
|
+
const dateFiltered = graph.entities.filter(e => {
|
|
125
104
|
const dateToCheck = e.createdAt || e.lastModified;
|
|
126
105
|
if (dateToCheck && !isWithinDateRange(dateToCheck, startDate, endDate)) {
|
|
127
106
|
return false;
|
|
128
107
|
}
|
|
129
|
-
// Entity type filter
|
|
130
|
-
if (entityType && e.entityType !== entityType) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
// Tags filter
|
|
134
|
-
if (normalizedTags && normalizedTags.length > 0) {
|
|
135
|
-
if (!e.tags || e.tags.length === 0)
|
|
136
|
-
return false;
|
|
137
|
-
const entityTags = e.tags.map(tag => tag.toLowerCase());
|
|
138
|
-
const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
|
|
139
|
-
if (!hasMatchingTag)
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
108
|
return true;
|
|
143
109
|
});
|
|
144
|
-
// Apply
|
|
145
|
-
const
|
|
110
|
+
// Apply entity type and tag filters using SearchFilterChain
|
|
111
|
+
const filters = { tags, entityType };
|
|
112
|
+
const filteredEntities = SearchFilterChain.applyFilters(dateFiltered, filters);
|
|
113
|
+
// Apply pagination using SearchFilterChain
|
|
114
|
+
const pagination = SearchFilterChain.validatePagination(offset, limit);
|
|
115
|
+
const paginatedEntities = SearchFilterChain.paginate(filteredEntities, pagination);
|
|
146
116
|
const filteredEntityNames = new Set(paginatedEntities.map(e => e.name));
|
|
147
117
|
const filteredRelations = graph.relations.filter(r => {
|
|
148
118
|
const dateToCheck = r.createdAt || r.lastModified;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { SEARCH_LIMITS, QUERY_LIMITS } from '../utils/constants.js';
|
|
9
9
|
import { ValidationError } from '../utils/errors.js';
|
|
10
|
+
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
10
11
|
/**
|
|
11
12
|
* Performs boolean search with query parsing and AST evaluation.
|
|
12
13
|
*/
|
|
@@ -48,36 +49,14 @@ export class BooleanSearch {
|
|
|
48
49
|
}
|
|
49
50
|
// Validate query complexity
|
|
50
51
|
this.validateQueryComplexity(queryAst);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
// Apply tag filter
|
|
62
|
-
if (normalizedTags && normalizedTags.length > 0) {
|
|
63
|
-
if (!e.tags || e.tags.length === 0)
|
|
64
|
-
return false;
|
|
65
|
-
const entityTags = e.tags.map(tag => tag.toLowerCase());
|
|
66
|
-
const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
|
|
67
|
-
if (!hasMatchingTag)
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
// Apply importance filter
|
|
71
|
-
if (minImportance !== undefined && (e.importance === undefined || e.importance < minImportance)) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
if (maxImportance !== undefined && (e.importance === undefined || e.importance > maxImportance)) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
});
|
|
79
|
-
// Apply pagination
|
|
80
|
-
const paginatedEntities = filteredEntities.slice(validatedOffset, validatedOffset + validatedLimit);
|
|
52
|
+
// First filter by boolean query evaluation (search-specific)
|
|
53
|
+
const booleanMatched = graph.entities.filter(e => this.evaluateBooleanQuery(queryAst, e));
|
|
54
|
+
// Apply tag and importance filters using SearchFilterChain
|
|
55
|
+
const filters = { tags, minImportance, maxImportance };
|
|
56
|
+
const filteredEntities = SearchFilterChain.applyFilters(booleanMatched, filters);
|
|
57
|
+
// Apply pagination using SearchFilterChain
|
|
58
|
+
const pagination = SearchFilterChain.validatePagination(offset, limit);
|
|
59
|
+
const paginatedEntities = SearchFilterChain.paginate(filteredEntities, pagination);
|
|
81
60
|
const filteredEntityNames = new Set(paginatedEntities.map(e => e.name));
|
|
82
61
|
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
83
62
|
return { entities: paginatedEntities, relations: filteredRelations };
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { levenshteinDistance } from '../utils/levenshtein.js';
|
|
9
9
|
import { SEARCH_LIMITS } from '../utils/constants.js';
|
|
10
|
+
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
10
11
|
/**
|
|
11
12
|
* Default fuzzy search similarity threshold (70% match required).
|
|
12
13
|
* Lower values are more permissive (more typos tolerated).
|
|
@@ -38,14 +39,9 @@ export class FuzzySearch {
|
|
|
38
39
|
*/
|
|
39
40
|
async fuzzySearch(query, threshold = DEFAULT_FUZZY_THRESHOLD, tags, minImportance, maxImportance, offset = 0, limit = SEARCH_LIMITS.DEFAULT) {
|
|
40
41
|
const graph = await this.storage.loadGraph();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const validatedLimit = Math.min(Math.max(SEARCH_LIMITS.MIN, limit), SEARCH_LIMITS.MAX);
|
|
45
|
-
// Filter entities using fuzzy matching
|
|
46
|
-
const filteredEntities = graph.entities.filter(e => {
|
|
47
|
-
// Fuzzy text search
|
|
48
|
-
const matchesQuery = this.isFuzzyMatch(e.name, query, threshold) ||
|
|
42
|
+
// First filter by fuzzy text match (search-specific)
|
|
43
|
+
const fuzzyMatched = graph.entities.filter(e => {
|
|
44
|
+
return (this.isFuzzyMatch(e.name, query, threshold) ||
|
|
49
45
|
this.isFuzzyMatch(e.entityType, query, threshold) ||
|
|
50
46
|
e.observations.some(o =>
|
|
51
47
|
// For observations, split into words and check each word
|
|
@@ -54,29 +50,14 @@ export class FuzzySearch {
|
|
|
54
50
|
.split(/\s+/)
|
|
55
51
|
.some(word => this.isFuzzyMatch(word, query, threshold)) ||
|
|
56
52
|
// Also check if the observation contains the query
|
|
57
|
-
this.isFuzzyMatch(o, query, threshold));
|
|
58
|
-
if (!matchesQuery)
|
|
59
|
-
return false;
|
|
60
|
-
// Tag filter
|
|
61
|
-
if (normalizedTags && normalizedTags.length > 0) {
|
|
62
|
-
if (!e.tags || e.tags.length === 0)
|
|
63
|
-
return false;
|
|
64
|
-
const entityTags = e.tags.map(tag => tag.toLowerCase());
|
|
65
|
-
const hasMatchingTag = normalizedTags.some(tag => entityTags.includes(tag));
|
|
66
|
-
if (!hasMatchingTag)
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
// Importance filter
|
|
70
|
-
if (minImportance !== undefined && (e.importance === undefined || e.importance < minImportance)) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
if (maxImportance !== undefined && (e.importance === undefined || e.importance > maxImportance)) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
return true;
|
|
53
|
+
this.isFuzzyMatch(o, query, threshold)));
|
|
77
54
|
});
|
|
78
|
-
// Apply
|
|
79
|
-
const
|
|
55
|
+
// Apply tag and importance filters using SearchFilterChain
|
|
56
|
+
const filters = { tags, minImportance, maxImportance };
|
|
57
|
+
const filteredEntities = SearchFilterChain.applyFilters(fuzzyMatched, filters);
|
|
58
|
+
// Apply pagination using SearchFilterChain
|
|
59
|
+
const pagination = SearchFilterChain.validatePagination(offset, limit);
|
|
60
|
+
const paginatedEntities = SearchFilterChain.paginate(filteredEntities, pagination);
|
|
80
61
|
const filteredEntityNames = new Set(paginatedEntities.map(e => e.name));
|
|
81
62
|
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
82
63
|
return {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { calculateTFIDF, tokenize } from '../utils/tfidf.js';
|
|
9
9
|
import { SEARCH_LIMITS } from '../utils/constants.js';
|
|
10
10
|
import { TFIDFIndexManager } from './TFIDFIndexManager.js';
|
|
11
|
+
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
11
12
|
/**
|
|
12
13
|
* Performs TF-IDF ranked search with optional pre-calculated indexes.
|
|
13
14
|
*/
|
|
@@ -78,26 +79,9 @@ export class RankedSearch {
|
|
|
78
79
|
// Enforce maximum search limit
|
|
79
80
|
const effectiveLimit = Math.min(limit, SEARCH_LIMITS.MAX);
|
|
80
81
|
const graph = await this.storage.loadGraph();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const filteredEntities = graph.entities
|
|
84
|
-
// Tag filter
|
|
85
|
-
if (normalizedTags && normalizedTags.length > 0) {
|
|
86
|
-
if (!e.tags || e.tags.length === 0)
|
|
87
|
-
return false;
|
|
88
|
-
const entityTags = e.tags.map(tag => tag.toLowerCase());
|
|
89
|
-
if (!normalizedTags.some(tag => entityTags.includes(tag)))
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
// Importance filter
|
|
93
|
-
if (minImportance !== undefined && (e.importance === undefined || e.importance < minImportance)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
if (maxImportance !== undefined && (e.importance === undefined || e.importance > maxImportance)) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return true;
|
|
100
|
-
});
|
|
82
|
+
// Apply tag and importance filters using SearchFilterChain
|
|
83
|
+
const filters = { tags, minImportance, maxImportance };
|
|
84
|
+
const filteredEntities = SearchFilterChain.applyFilters(graph.entities, filters);
|
|
101
85
|
// Try to use pre-calculated index
|
|
102
86
|
const index = await this.ensureIndexLoaded();
|
|
103
87
|
const queryTerms = tokenize(query);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Filter Chain
|
|
3
|
+
*
|
|
4
|
+
* Centralizes filter logic for all search implementations to eliminate
|
|
5
|
+
* duplicate filtering code across BasicSearch, BooleanSearch, FuzzySearch,
|
|
6
|
+
* and RankedSearch.
|
|
7
|
+
*
|
|
8
|
+
* @module search/SearchFilterChain
|
|
9
|
+
*/
|
|
10
|
+
import { normalizeTags, hasMatchingTag } from '../utils/tagUtils.js';
|
|
11
|
+
import { isWithinImportanceRange } from '../utils/filterUtils.js';
|
|
12
|
+
import { validatePagination, applyPagination } from '../utils/paginationUtils.js';
|
|
13
|
+
/**
|
|
14
|
+
* Centralized filter chain for all search implementations.
|
|
15
|
+
* Ensures consistent filtering behavior across search types.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const filters: SearchFilters = { tags: ['important'], minImportance: 5 };
|
|
20
|
+
* const filtered = SearchFilterChain.applyFilters(entities, filters);
|
|
21
|
+
* const pagination = SearchFilterChain.validatePagination(0, 50);
|
|
22
|
+
* const result = SearchFilterChain.paginate(filtered, pagination);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class SearchFilterChain {
|
|
26
|
+
/**
|
|
27
|
+
* Applies all filters to an array of entities.
|
|
28
|
+
* Entities must pass ALL specified filters to be included.
|
|
29
|
+
*
|
|
30
|
+
* @param entities - Entities to filter
|
|
31
|
+
* @param filters - Filter criteria to apply
|
|
32
|
+
* @returns Filtered entities array
|
|
33
|
+
*/
|
|
34
|
+
static applyFilters(entities, filters) {
|
|
35
|
+
// Early return if no filters are active
|
|
36
|
+
if (!this.hasActiveFilters(filters)) {
|
|
37
|
+
return entities;
|
|
38
|
+
}
|
|
39
|
+
// Pre-normalize tags once for efficiency
|
|
40
|
+
const normalizedSearchTags = filters.tags?.length
|
|
41
|
+
? normalizeTags(filters.tags)
|
|
42
|
+
: undefined;
|
|
43
|
+
return entities.filter(entity => this.entityPassesFilters(entity, filters, normalizedSearchTags));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks if an entity passes all active filters.
|
|
47
|
+
* Short-circuits on first failing filter for performance.
|
|
48
|
+
*
|
|
49
|
+
* @param entity - Entity to check
|
|
50
|
+
* @param filters - Filter criteria
|
|
51
|
+
* @param normalizedSearchTags - Pre-normalized search tags (for efficiency)
|
|
52
|
+
* @returns true if entity passes all filters
|
|
53
|
+
*/
|
|
54
|
+
static entityPassesFilters(entity, filters, normalizedSearchTags) {
|
|
55
|
+
// Tag filter - check if entity has any matching tag
|
|
56
|
+
if (normalizedSearchTags && normalizedSearchTags.length > 0) {
|
|
57
|
+
if (!entity.tags || entity.tags.length === 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const entityTags = normalizeTags(entity.tags);
|
|
61
|
+
const hasMatch = normalizedSearchTags.some(tag => entityTags.includes(tag));
|
|
62
|
+
if (!hasMatch) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Importance filter
|
|
67
|
+
if (!isWithinImportanceRange(entity.importance, filters.minImportance, filters.maxImportance)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Entity type filter
|
|
71
|
+
if (filters.entityType && entity.entityType !== filters.entityType) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// Created date filter
|
|
75
|
+
if (filters.createdAfter || filters.createdBefore) {
|
|
76
|
+
if (!entity.createdAt) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const createdAt = new Date(entity.createdAt);
|
|
80
|
+
if (filters.createdAfter && createdAt < new Date(filters.createdAfter)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (filters.createdBefore && createdAt > new Date(filters.createdBefore)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Modified date filter
|
|
88
|
+
if (filters.modifiedAfter || filters.modifiedBefore) {
|
|
89
|
+
if (!entity.lastModified) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const modifiedAt = new Date(entity.lastModified);
|
|
93
|
+
if (filters.modifiedAfter && modifiedAt < new Date(filters.modifiedAfter)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (filters.modifiedBefore && modifiedAt > new Date(filters.modifiedBefore)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Checks if any filters are actually specified.
|
|
104
|
+
* Used for early return optimization.
|
|
105
|
+
*
|
|
106
|
+
* @param filters - Filter criteria to check
|
|
107
|
+
* @returns true if at least one filter is active
|
|
108
|
+
*/
|
|
109
|
+
static hasActiveFilters(filters) {
|
|
110
|
+
return !!((filters.tags && filters.tags.length > 0) ||
|
|
111
|
+
filters.minImportance !== undefined ||
|
|
112
|
+
filters.maxImportance !== undefined ||
|
|
113
|
+
filters.entityType ||
|
|
114
|
+
filters.createdAfter ||
|
|
115
|
+
filters.createdBefore ||
|
|
116
|
+
filters.modifiedAfter ||
|
|
117
|
+
filters.modifiedBefore);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validates and returns pagination parameters.
|
|
121
|
+
* Delegates to paginationUtils.validatePagination.
|
|
122
|
+
*
|
|
123
|
+
* @param offset - Starting position
|
|
124
|
+
* @param limit - Maximum results
|
|
125
|
+
* @returns Validated pagination object
|
|
126
|
+
*/
|
|
127
|
+
static validatePagination(offset = 0, limit) {
|
|
128
|
+
return validatePagination(offset, limit);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Applies pagination to a filtered result set.
|
|
132
|
+
*
|
|
133
|
+
* @param entities - Entities to paginate
|
|
134
|
+
* @param pagination - Validated pagination parameters
|
|
135
|
+
* @returns Paginated slice of entities
|
|
136
|
+
*/
|
|
137
|
+
static paginate(entities, pagination) {
|
|
138
|
+
return applyPagination(entities, pagination);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convenience method to apply both filters and pagination in one call.
|
|
142
|
+
*
|
|
143
|
+
* @param entities - Entities to process
|
|
144
|
+
* @param filters - Filter criteria
|
|
145
|
+
* @param offset - Pagination offset
|
|
146
|
+
* @param limit - Pagination limit
|
|
147
|
+
* @returns Filtered and paginated entities
|
|
148
|
+
*/
|
|
149
|
+
static filterAndPaginate(entities, filters, offset = 0, limit) {
|
|
150
|
+
const filtered = this.applyFilters(entities, filters);
|
|
151
|
+
const pagination = this.validatePagination(offset, limit);
|
|
152
|
+
return this.paginate(filtered, pagination);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Applies tag filter only. Useful when other filters are handled separately.
|
|
156
|
+
*
|
|
157
|
+
* @param entities - Entities to filter
|
|
158
|
+
* @param tags - Tags to filter by
|
|
159
|
+
* @returns Filtered entities
|
|
160
|
+
*/
|
|
161
|
+
static filterByTags(entities, tags) {
|
|
162
|
+
if (!tags || tags.length === 0) {
|
|
163
|
+
return entities;
|
|
164
|
+
}
|
|
165
|
+
const normalizedTags = normalizeTags(tags);
|
|
166
|
+
return entities.filter(entity => {
|
|
167
|
+
if (!entity.tags || entity.tags.length === 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return hasMatchingTag(entity.tags, normalizedTags);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Applies importance filter only. Useful when other filters are handled separately.
|
|
175
|
+
*
|
|
176
|
+
* @param entities - Entities to filter
|
|
177
|
+
* @param minImportance - Minimum importance
|
|
178
|
+
* @param maxImportance - Maximum importance
|
|
179
|
+
* @returns Filtered entities
|
|
180
|
+
*/
|
|
181
|
+
static filterByImportance(entities, minImportance, maxImportance) {
|
|
182
|
+
if (minImportance === undefined && maxImportance === undefined) {
|
|
183
|
+
return entities;
|
|
184
|
+
}
|
|
185
|
+
return entities.filter(entity => isWithinImportanceRange(entity.importance, minImportance, maxImportance));
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/search/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search Module Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Sprint 2: Added SearchFilterChain for centralized filter logic
|
|
3
5
|
*/
|
|
4
6
|
export { BasicSearch } from './BasicSearch.js';
|
|
5
7
|
export { RankedSearch } from './RankedSearch.js';
|
|
@@ -8,3 +10,5 @@ export { FuzzySearch } from './FuzzySearch.js';
|
|
|
8
10
|
export { SearchSuggestions } from './SearchSuggestions.js';
|
|
9
11
|
export { SavedSearchManager } from './SavedSearchManager.js';
|
|
10
12
|
export { SearchManager } from './SearchManager.js';
|
|
13
|
+
// Sprint 2: Search Filter Chain utilities
|
|
14
|
+
export { SearchFilterChain } from './SearchFilterChain.js';
|