search-algoritm 1.4.1 → 1.5.1
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/CHANGELOG.md +10 -8
- package/index.js +1 -2
- package/package.json +1 -1
- package/src/searchAlgoritm.js +130 -126
package/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
## [1.5.0] - 2026-03-05
|
|
7
|
+
### Changed
|
|
8
|
+
- Fixed default export for easier import: `const searchAlgoritm = require('search-algoritm')`.
|
|
9
|
+
- Consolidated all helper functions (`normalize`, `levenshtein`, `tokenize`, `matchScore`, `calculateItemScore`) into the main module.
|
|
10
|
+
- Module now works out-of-the-box without requiring `src/searchAlgoritm.js`.
|
|
11
|
+
- Solves previous errors: `TypeError: searchAlgoritm is not a function` and `ReferenceError: normalize is not defined`.
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
---
|
|
8
14
|
## [1.4.1] - 2026-03-05
|
|
9
15
|
### Notice
|
|
10
16
|
- Update due to error in version. 1.3.0 never released.
|
|
11
17
|
|
|
12
|
-
|
|
13
18
|
---
|
|
14
19
|
## [1.4.0] - 2026-03-05
|
|
15
20
|
### Added
|
|
16
|
-
- Search
|
|
21
|
+
- Search algoritm now dynamically scans **all string fields** in any object.
|
|
17
22
|
- Fuzzy search, tokenization, and scoring applied across all fields.
|
|
18
23
|
- Stopwords filtering, normalization, and accent-insensitive search preserved.
|
|
19
24
|
|
|
@@ -51,7 +56,6 @@ All notable
|
|
|
51
56
|
- Added `version-status.js` and `postinstall` script to display the current package version status after installation (patch).
|
|
52
57
|
|
|
53
58
|
---
|
|
54
|
-
|
|
55
59
|
## [1.1.0] - 2025-12-01
|
|
56
60
|
### Added
|
|
57
61
|
- **Accent-insensitive search** (`é → e`) for better matching of text with diacritics.
|
|
@@ -64,13 +68,11 @@ All notable
|
|
|
64
68
|
- Improved fuzzy matching algoritm for more accurate relevance scoring.
|
|
65
69
|
|
|
66
70
|
---
|
|
67
|
-
|
|
68
71
|
## [1.0.1] - 2025-12-01
|
|
69
72
|
### Changed
|
|
70
73
|
- Bumped version due to NPM error preventing re-publishing of 1.0.0.
|
|
71
74
|
|
|
72
75
|
---
|
|
73
|
-
|
|
74
76
|
## [1.0.0] - 2025-12-01
|
|
75
77
|
### Added
|
|
76
78
|
- Initial release of **search-algoritm** npm package.
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/searchAlgoritm.js
CHANGED
|
@@ -1,143 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return matrix[b.length][a.length];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Stopwords
|
|
49
|
-
*/
|
|
50
|
-
const stopWords = new Set([
|
|
51
|
-
'and', 'the', 'of', 'a', 'i', 'in', 'on', 'for', 'with',
|
|
52
|
-
'en', 'ett', 'och', 'från', 'med', 'på', 'för'
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Tokenize text
|
|
57
|
-
*/
|
|
58
|
-
const tokenize = (text) =>
|
|
59
|
-
normalize(text)
|
|
60
|
-
.split(/\s+/)
|
|
61
|
-
.filter(word => word && !stopWords.has(word));
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Fuzzy match score
|
|
65
|
-
*/
|
|
66
|
-
const matchScore = (text, query) => {
|
|
67
|
-
if (!text || !query) return 0;
|
|
68
|
-
|
|
69
|
-
const queryTokens = tokenize(query);
|
|
70
|
-
const textTokens = tokenize(text);
|
|
71
|
-
|
|
72
|
-
if (!queryTokens.length || !textTokens.length) return 0;
|
|
73
|
-
|
|
74
|
-
let score = 0;
|
|
75
|
-
|
|
76
|
-
for (const qWord of queryTokens) {
|
|
77
|
-
if (textTokens.includes(qWord)) {
|
|
78
|
-
score += 25;
|
|
79
|
-
continue;
|
|
1
|
+
const searchAlgoritm = (() => {
|
|
2
|
+
console.log('[searchAlgoritm] 🔍 Module loaded');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalize text: lowercase, remove accents, trim, remove simple plurals
|
|
6
|
+
* @param {string} str
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
const normalize = (str = '') =>
|
|
10
|
+
String(str)
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.normalize('NFD')
|
|
13
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
14
|
+
.replace(/s$/, '')
|
|
15
|
+
.trim();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Levenshtein distance (edit distance)
|
|
19
|
+
*/
|
|
20
|
+
const levenshtein = (a, b) => {
|
|
21
|
+
if (a === b) return 0;
|
|
22
|
+
if (!a) return b.length;
|
|
23
|
+
if (!b) return a.length;
|
|
24
|
+
|
|
25
|
+
const matrix = Array.from({ length: b.length + 1 }, () =>
|
|
26
|
+
new Array(a.length + 1)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i <= b.length; i++) matrix[i][0] = i;
|
|
30
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
31
|
+
|
|
32
|
+
for (let i = 1; i <= b.length; i++) {
|
|
33
|
+
for (let j = 1; j <= a.length; j++) {
|
|
34
|
+
matrix[i][j] =
|
|
35
|
+
b[i - 1] === a[j - 1]
|
|
36
|
+
? matrix[i - 1][j - 1]
|
|
37
|
+
: Math.min(
|
|
38
|
+
matrix[i - 1][j - 1] + 1,
|
|
39
|
+
matrix[i][j - 1] + 1,
|
|
40
|
+
matrix[i - 1][j] + 1
|
|
41
|
+
);
|
|
42
|
+
}
|
|
80
43
|
}
|
|
81
44
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
45
|
+
return matrix[b.length][a.length];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stopwords
|
|
50
|
+
*/
|
|
51
|
+
const stopWords = new Set([
|
|
52
|
+
'and', 'the', 'of', 'a', 'i', 'in', 'on', 'for', 'with',
|
|
53
|
+
'en', 'ett', 'och', 'från', 'med', 'på', 'för'
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tokenize text
|
|
58
|
+
*/
|
|
59
|
+
const tokenize = (text) =>
|
|
60
|
+
normalize(text)
|
|
61
|
+
.split(/\s+/)
|
|
62
|
+
.filter(word => word && !stopWords.has(word));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fuzzy match score
|
|
66
|
+
*/
|
|
67
|
+
const matchScore = (text, query) => {
|
|
68
|
+
if (!text || !query) return 0;
|
|
69
|
+
|
|
70
|
+
const queryTokens = tokenize(query);
|
|
71
|
+
const textTokens = tokenize(text);
|
|
72
|
+
|
|
73
|
+
if (!queryTokens.length || !textTokens.length) return 0;
|
|
74
|
+
|
|
75
|
+
let score = 0;
|
|
76
|
+
|
|
77
|
+
for (const qWord of queryTokens) {
|
|
78
|
+
if (textTokens.includes(qWord)) {
|
|
79
|
+
score += 25;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
}
|
|
83
|
+
let bestSim = 0;
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
for (const tw of textTokens) {
|
|
86
|
+
const dist = levenshtein(tw, qWord);
|
|
87
|
+
const sim = (1 - dist / Math.max(tw.length, qWord.length)) * 100;
|
|
88
|
+
if (sim > bestSim) bestSim = sim;
|
|
89
|
+
}
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
}
|
|
91
|
+
if (bestSim > 50) score += bestSim / 2;
|
|
92
|
+
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const calculateItemScore = (item, query) => {
|
|
104
|
-
let score = 0;
|
|
94
|
+
if (textTokens.join(' ').includes(queryTokens.join(' '))) {
|
|
95
|
+
score += 20;
|
|
96
|
+
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
return Math.min(Math.round(score), 100);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Score item by scanning all string fields
|
|
103
|
+
*/
|
|
104
|
+
const calculateItemScore = (item, query) => {
|
|
105
|
+
let score = 0;
|
|
106
|
+
|
|
107
|
+
for (const key in item) {
|
|
108
|
+
if (typeof item[key] === 'string') {
|
|
109
|
+
const cacheKey = `_lc_${key}`;
|
|
110
|
+
if (!item[cacheKey]) {
|
|
111
|
+
item[cacheKey] = normalize(item[key]);
|
|
112
|
+
}
|
|
113
|
+
score += matchScore(item[cacheKey], query);
|
|
111
114
|
}
|
|
112
|
-
score += matchScore(item[cacheKey], query);
|
|
113
115
|
}
|
|
114
|
-
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
};
|
|
117
|
+
return score;
|
|
118
|
+
};
|
|
118
119
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const searchAlgoritm = (query, dataList, enableLog = false) => {
|
|
123
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Main search function
|
|
122
|
+
*/
|
|
123
|
+
const searchAlgoritm = (query, dataList, enableLog = false) => {
|
|
124
|
+
if (!query || !Array.isArray(dataList)) return [];
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
const normalizedQuery = normalize(query);
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
const results = dataList
|
|
129
|
+
.map(item => ({
|
|
130
|
+
...item,
|
|
131
|
+
_score: calculateItemScore(item, normalizedQuery)
|
|
132
|
+
}))
|
|
133
|
+
.filter(item => item._score > 0)
|
|
134
|
+
.sort((a, b) => b._score - a._score);
|
|
135
|
+
|
|
136
|
+
if (enableLog) {
|
|
137
|
+
console.log(`[searchAlgoritm] 🔎 Query: "${query}", results: ${results.length}`);
|
|
138
|
+
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
140
|
+
return results;
|
|
141
|
+
};
|
|
138
142
|
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
// Exponera funktionerna du behöver
|
|
144
|
+
return searchAlgoritm;
|
|
145
|
+
})();
|
|
141
146
|
|
|
142
|
-
/* Default export */
|
|
143
147
|
module.exports = searchAlgoritm;
|