search-algoritm 1.0.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 +18 -0
- package/Example files/data.json +6 -0
- package/Example files/index.html +21 -0
- package/Example files/ip-adress.js +3 -0
- package/Example files/json-server.js +55 -0
- package/Example files/package.json +13 -0
- package/Example files/search.js +54 -0
- package/Example files/server.js +65 -0
- package/Example files/style.css +56 -0
- package/LICENSE +14 -0
- package/README.md +201 -0
- package/index.js +4 -0
- package/package.json +27 -0
- package/src/searchAlgoritm.js +140 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## [1.0.1] - 2025-12-01
|
|
7
|
+
- Bumped version due to NPM error.
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2025-12-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Initial release of **search-algoritm** npm package.
|
|
13
|
+
- Node.js and browser usage examples.
|
|
14
|
+
- Example files included in `Example Files` folder.
|
|
15
|
+
- Basic fuzzy search algorithm supporting `title` and `description` fields.
|
|
16
|
+
- **Levenshtein distance support** for fuzzy matching.
|
|
17
|
+
- README.md with installation instructions and usage examples.
|
|
18
|
+
- License file (MIT).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Search Example</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<div class="page-wrapper">
|
|
12
|
+
<div class="search-wrapper">
|
|
13
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search...">
|
|
14
|
+
<button id="searchBtn" class="search-btn">Search</button>
|
|
15
|
+
</div>
|
|
16
|
+
<ul id="searchResults" class="search-results"></ul>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<script type="module" src="./search.js"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { searchAlgoritm } = require('search-algoritm');
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const PORT = 3000;
|
|
8
|
+
|
|
9
|
+
// Directory containing the static frontend files
|
|
10
|
+
const staticPath = path.join(__dirname, 'Example files');
|
|
11
|
+
app.use(express.static(staticPath));
|
|
12
|
+
|
|
13
|
+
// Path to the JSON dataset used by the search engine
|
|
14
|
+
const dataPath = path.join(__dirname, 'data.json');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads and parses a JSON file asynchronously.
|
|
18
|
+
* If reading or parsing fails, an empty array is returned instead,
|
|
19
|
+
* ensuring the server remains stable.
|
|
20
|
+
*/
|
|
21
|
+
const loadJson = async (filePath) => {
|
|
22
|
+
try {
|
|
23
|
+
const rawData = await fs.readFile(filePath, 'utf-8');
|
|
24
|
+
return JSON.parse(rawData);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[server] Error loading JSON file:', err);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Search API endpoint
|
|
33
|
+
* Example: GET /api/search?q=example
|
|
34
|
+
*
|
|
35
|
+
* Loads the JSON file for each request, runs the search algorithm,
|
|
36
|
+
* and returns the results along with the original query.
|
|
37
|
+
*/
|
|
38
|
+
app.get('/api/search', async (req, res) => {
|
|
39
|
+
const query = req.query.q || "";
|
|
40
|
+
|
|
41
|
+
// Load data from disk
|
|
42
|
+
const searchData = await loadJson(dataPath);
|
|
43
|
+
|
|
44
|
+
// Execute search
|
|
45
|
+
const results = searchAlgoritm(query, searchData);
|
|
46
|
+
|
|
47
|
+
res.json({ query, results });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start the Express server
|
|
52
|
+
*/
|
|
53
|
+
app.listen(PORT, () => {
|
|
54
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
55
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-files",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "search.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
+
"start": "node server.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"description": ""
|
|
13
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Import the backend server URL from a separate module
|
|
2
|
+
import backendIP from './ip-adress.js';
|
|
3
|
+
|
|
4
|
+
// Initialize the search functionality
|
|
5
|
+
async function initSearch() {
|
|
6
|
+
// Get references to the input field, search button, and results container
|
|
7
|
+
const searchInput = document.getElementById('searchInput');
|
|
8
|
+
const searchBtn = document.getElementById('searchBtn');
|
|
9
|
+
const resultsList = document.getElementById('searchResults');
|
|
10
|
+
|
|
11
|
+
// Function to perform the search when called
|
|
12
|
+
async function performSearch() {
|
|
13
|
+
// Get the input value and remove extra spaces
|
|
14
|
+
const query = searchInput.value.trim();
|
|
15
|
+
|
|
16
|
+
// If the input is empty, show a message and stop
|
|
17
|
+
if (!query) {
|
|
18
|
+
resultsList.innerHTML = `<li class="no-result">Please enter a search term</li>`;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Fetch search results from the backend API
|
|
24
|
+
const res = await fetch(`${backendIP}/api/search?q=${encodeURIComponent(query)}`);
|
|
25
|
+
// Parse the JSON response
|
|
26
|
+
const { results } = await res.json();
|
|
27
|
+
|
|
28
|
+
// Render results in the DOM
|
|
29
|
+
resultsList.innerHTML = results.length
|
|
30
|
+
? results.map(item => `
|
|
31
|
+
<li class="result-item">
|
|
32
|
+
<strong>${item.title}</strong><br>
|
|
33
|
+
<span>${item.description}</span>
|
|
34
|
+
</li>
|
|
35
|
+
`).join('') // Join all results into a single string
|
|
36
|
+
: `<li class="no-result">No matches found</li>`; // Show message if no results
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Handle any errors from the fetch call
|
|
39
|
+
console.error('Error fetching search results:', err);
|
|
40
|
+
resultsList.innerHTML = `<li class="no-result">Could not fetch results</li>`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Trigger search when the user presses Enter in the input field
|
|
45
|
+
searchInput.addEventListener('keydown', e => {
|
|
46
|
+
if (e.key === 'Enter') performSearch();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Trigger search when the user clicks the search button
|
|
50
|
+
searchBtn.addEventListener('click', performSearch);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Call the function to initialize search when the script loads
|
|
54
|
+
initSearch();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// server.js
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const { searchAlgoritm } = require('search-algoritm');
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const PORT = 3000;
|
|
9
|
+
|
|
10
|
+
// Path to the JSON file containing searchable data
|
|
11
|
+
const dataPath = path.join(__dirname, 'data.json');
|
|
12
|
+
|
|
13
|
+
// In-memory cache for search data
|
|
14
|
+
let searchData = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load the JSON file into memory.
|
|
18
|
+
* This reduces file I/O and makes searching faster.
|
|
19
|
+
*/
|
|
20
|
+
const loadData = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const rawData = await fs.readFile(dataPath, 'utf-8');
|
|
23
|
+
searchData = JSON.parse(rawData);
|
|
24
|
+
console.log(`[server] Loaded ${searchData.length} items into cache`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[server] Failed to load data.json:', err);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Load data on startup
|
|
31
|
+
loadData();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optional:
|
|
35
|
+
* Automatically reload the cached data whenever the file changes.
|
|
36
|
+
* This ensures the server always uses the latest data without restarting.
|
|
37
|
+
*/
|
|
38
|
+
fs.watchFile(dataPath, async () => {
|
|
39
|
+
console.log('[server] data.json changed — reloading cache...');
|
|
40
|
+
await loadData();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Serve static frontend files
|
|
44
|
+
app.use(express.static(path.join(__dirname, 'Example files')));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Search API endpoint
|
|
48
|
+
* Example: GET /api/search?q=hello
|
|
49
|
+
*/
|
|
50
|
+
app.get('/api/search', (req, res) => {
|
|
51
|
+
const query = (req.query.q || "").trim();
|
|
52
|
+
|
|
53
|
+
// Empty query → return an empty result set
|
|
54
|
+
if (!query) return res.json({ query, results: [] });
|
|
55
|
+
|
|
56
|
+
// Perform search on the cached list
|
|
57
|
+
const results = searchAlgoritm(query, searchData);
|
|
58
|
+
|
|
59
|
+
res.json({ query, results });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Start the server
|
|
63
|
+
app.listen(PORT, () => {
|
|
64
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
65
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font-family: Arial, sans-serif;
|
|
3
|
+
background: #0f0f0f;
|
|
4
|
+
color: #e4e4e4;
|
|
5
|
+
display: flex;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
align-items: center;
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
margin: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.page-wrapper {
|
|
13
|
+
max-width: 500px;
|
|
14
|
+
width: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.search-wrapper {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
margin-bottom: 12px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.search-input {
|
|
24
|
+
flex: 1;
|
|
25
|
+
padding: 10px;
|
|
26
|
+
border-radius: 6px;
|
|
27
|
+
border: 1px solid #444;
|
|
28
|
+
background: #1b1b1b;
|
|
29
|
+
color: #e6e6e6;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.search-btn {
|
|
33
|
+
padding: 10px 16px;
|
|
34
|
+
border: none;
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
background: #3b82f6;
|
|
37
|
+
color: white;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.search-results {
|
|
42
|
+
list-style: none;
|
|
43
|
+
padding: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.result-item {
|
|
47
|
+
padding: 10px;
|
|
48
|
+
background: #181818;
|
|
49
|
+
margin-bottom: 6px;
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.no-result {
|
|
54
|
+
color: #f87171;
|
|
55
|
+
padding: 10px;
|
|
56
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2025 Felix Lind
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is granted to install, use, and execute this software as provided via npm or other official distribution channels. This includes use in personal, internal, or commercial projects as part of a larger application or website.
|
|
6
|
+
|
|
7
|
+
You MAY NOT:
|
|
8
|
+
- Sell, redistribute, or sublicense this software as a standalone product.
|
|
9
|
+
- Modify and distribute modified versions of this software as a standalone product.
|
|
10
|
+
- Extract, copy, or otherwise distribute the source code outside of the intended installation/usage.
|
|
11
|
+
|
|
12
|
+
This software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the author be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or its use.
|
|
13
|
+
|
|
14
|
+
By installing or using this software, you agree to comply with the above restrictions.
|
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
````markdown
|
|
2
|
+
# search-algoritm
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
[](https://github.com/FelixLind1/SearchAlgoritm)
|
|
8
|
+
|
|
9
|
+
A lightweight Node.js library for **fuzzy searching** arrays of objects.
|
|
10
|
+
It calculates relevance scores based on how well a query matches the `title` and `description` of each item.
|
|
11
|
+
|
|
12
|
+
**Features:**
|
|
13
|
+
|
|
14
|
+
- Fuzzy matching using **Levenshtein distance**
|
|
15
|
+
- **Accent-insensitive** searches (`é → e`)
|
|
16
|
+
- Token-based word matching for partial or multi-word queries
|
|
17
|
+
- Weighted scoring: title matches are more important than description
|
|
18
|
+
- Stopword filtering for more relevant results
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install search-algoritm
|
|
26
|
+
````
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Node.js Usage (Server-side)
|
|
31
|
+
|
|
32
|
+
This library supports **two server setups** depending on your dataset size and update frequency:
|
|
33
|
+
|
|
34
|
+
1. **Cached JSON Server** – loads JSON into memory once, reloads if the file changes (fast for large datasets).
|
|
35
|
+
2. **Dynamic JSON Server** – reads JSON from disk on each request (simple, slower for large datasets).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### 1. Cached JSON Server (`server.js`)
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const express = require('express');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const fs = require('fs').promises;
|
|
45
|
+
const { searchAlgoritm } = require('search-algoritm');
|
|
46
|
+
|
|
47
|
+
const app = express();
|
|
48
|
+
const PORT = 3000;
|
|
49
|
+
const dataPath = path.join(__dirname, 'data.json');
|
|
50
|
+
|
|
51
|
+
let searchData = [];
|
|
52
|
+
|
|
53
|
+
// Load data into memory
|
|
54
|
+
const loadData = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const rawData = await fs.readFile(dataPath, 'utf-8');
|
|
57
|
+
searchData = JSON.parse(rawData);
|
|
58
|
+
console.log(`[server] Loaded ${searchData.length} items`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('[server] Failed to load data.json:', err);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
loadData();
|
|
65
|
+
|
|
66
|
+
// Reload cache if file changes
|
|
67
|
+
fs.watchFile(dataPath, async () => {
|
|
68
|
+
console.log('[server] data.json changed, reloading cache...');
|
|
69
|
+
await loadData();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.use(express.static(path.join(__dirname, 'Example files')));
|
|
73
|
+
|
|
74
|
+
app.get('/api/search', async (req, res) => {
|
|
75
|
+
const query = (req.query.q || "").trim();
|
|
76
|
+
if (!query) return res.json({ query, results: [] });
|
|
77
|
+
|
|
78
|
+
const results = searchAlgoritm(query, searchData);
|
|
79
|
+
res.json({ query, results });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
app.listen(PORT, () => {
|
|
83
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
✅ Fast searches using in-memory cache. Ideal for **medium to large datasets** where performance matters.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### 2. Dynamic JSON Server (`json-server.js`)
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const express = require('express');
|
|
95
|
+
const path = require('path');
|
|
96
|
+
const fs = require('fs').promises;
|
|
97
|
+
const { searchAlgoritm } = require('search-algoritm');
|
|
98
|
+
|
|
99
|
+
const app = express();
|
|
100
|
+
const PORT = 3000;
|
|
101
|
+
|
|
102
|
+
app.use(express.static(path.join(__dirname, 'Example files')));
|
|
103
|
+
const dataPath = path.join(__dirname, 'data.json');
|
|
104
|
+
|
|
105
|
+
const loadJson = async (filePath) => {
|
|
106
|
+
try {
|
|
107
|
+
const rawData = await fs.readFile(filePath, 'utf-8');
|
|
108
|
+
return JSON.parse(rawData);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Error reading JSON file:', err);
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
app.get('/api/search', async (req, res) => {
|
|
116
|
+
const query = req.query.q || "";
|
|
117
|
+
const searchData = await loadJson(dataPath);
|
|
118
|
+
const results = searchAlgoritm(query, searchData);
|
|
119
|
+
res.json({ query, results });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
app.listen(PORT, () => {
|
|
123
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
✅ Reads JSON on each request. Best for **small datasets** or when data changes frequently.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Best Practices
|
|
132
|
+
|
|
133
|
+
| Server Type | Pros | Cons | When to Use |
|
|
134
|
+
| ------------ | ------------------------------- | ---------------------------------------------- | ------------------------------------------- |
|
|
135
|
+
| Cached JSON | Fast searches, reduces disk I/O | Uses more memory, needs cache reload on change | Medium to large datasets, frequent searches |
|
|
136
|
+
| Dynamic JSON | Always fresh data, simple setup | Slower for large datasets | Small datasets or frequently changing data |
|
|
137
|
+
|
|
138
|
+
**Tip:** For production with large datasets, use the **cached server**. For prototypes or rapidly changing content, use the **dynamic server**.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Frontend Usage (Browser)
|
|
143
|
+
|
|
144
|
+
Always fetch search results from the server.
|
|
145
|
+
|
|
146
|
+
### `ip-adress.js`
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const backendIP = 'http://localhost:3000';
|
|
150
|
+
export default backendIP;
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `search.js`
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import backendIP from './ip-adress.js';
|
|
157
|
+
|
|
158
|
+
async function initSearch() {
|
|
159
|
+
const searchInput = document.getElementById('searchInput');
|
|
160
|
+
const searchBtn = document.getElementById('searchBtn');
|
|
161
|
+
const resultsList = document.getElementById('searchResults');
|
|
162
|
+
|
|
163
|
+
async function performSearch() {
|
|
164
|
+
const query = searchInput.value.trim();
|
|
165
|
+
if (!query) {
|
|
166
|
+
resultsList.innerHTML = `<li class="no-result">Please enter a search term</li>`;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`${backendIP}/api/search?q=${encodeURIComponent(query)}`);
|
|
172
|
+
const { results } = await res.json();
|
|
173
|
+
|
|
174
|
+
resultsList.innerHTML = results.length
|
|
175
|
+
? results.map(item => `
|
|
176
|
+
<li class="result-item">
|
|
177
|
+
<strong>${item.title}</strong><br>
|
|
178
|
+
<span>${item.description}</span>
|
|
179
|
+
</li>
|
|
180
|
+
`).join('')
|
|
181
|
+
: `<li class="no-result">No matches found</li>`;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('Error fetching search results:', err);
|
|
184
|
+
resultsList.innerHTML = `<li class="no-result">Could not fetch results</li>`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') performSearch(); });
|
|
189
|
+
searchBtn.addEventListener('click', performSearch);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
initSearch();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
200
|
+
|
|
201
|
+
**Made by [Felix Lind](https://github.com/FelixLind1)**
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search-algoritm",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Simple and lightweight fuzzy search algoritm for titles and descriptions.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"search",
|
|
11
|
+
"fuzzy",
|
|
12
|
+
"algoritm",
|
|
13
|
+
"filter",
|
|
14
|
+
"javascript",
|
|
15
|
+
"node"
|
|
16
|
+
],
|
|
17
|
+
"author": "Felix Lind",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/FelixLind1/SearchAlgoritm.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/FelixLind1/SearchAlgoritm/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/FelixLind1/SearchAlgoritm/blob/main/README.md"
|
|
27
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// searchAlgoritm.js
|
|
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
|
+
str
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.normalize('NFD') // Normalize unicode (accents)
|
|
13
|
+
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics (é → e)
|
|
14
|
+
.replace(/s$/, '') // Remove trailing 's' (simple plural)
|
|
15
|
+
.trim();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Levenshtein distance (edit distance)
|
|
19
|
+
* Measures how many edits are needed to transform string a into b
|
|
20
|
+
* @param {string} a
|
|
21
|
+
* @param {string} b
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
const levenshtein = (a, b) => {
|
|
25
|
+
if (a === b) return 0;
|
|
26
|
+
if (!a) return b.length;
|
|
27
|
+
if (!b) return a.length;
|
|
28
|
+
|
|
29
|
+
const matrix = Array.from({ length: b.length + 1 }, (_, i) => [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] = b[i - 1] === a[j - 1]
|
|
35
|
+
? matrix[i - 1][j - 1]
|
|
36
|
+
: Math.min(
|
|
37
|
+
matrix[i - 1][j - 1] + 1, // substitution
|
|
38
|
+
matrix[i][j - 1] + 1, // insertion
|
|
39
|
+
matrix[i - 1][j] + 1 // deletion
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return matrix[b.length][a.length];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Common stopwords to ignore during search
|
|
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 a string into words, normalize, and remove stopwords
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @returns {Array<string>}
|
|
59
|
+
*/
|
|
60
|
+
const tokenize = (text) =>
|
|
61
|
+
normalize(text)
|
|
62
|
+
.split(/\s+/)
|
|
63
|
+
.filter(word => word && !stopWords.has(word));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute fuzzy match score between a text and a query (0-100)
|
|
67
|
+
* @param {string} text
|
|
68
|
+
* @param {string} query
|
|
69
|
+
* @returns {number}
|
|
70
|
+
*/
|
|
71
|
+
const matchScore = (text, query) => {
|
|
72
|
+
if (!text || !query) return 0;
|
|
73
|
+
|
|
74
|
+
const queryTokens = tokenize(query);
|
|
75
|
+
const textTokens = tokenize(text);
|
|
76
|
+
let score = 0;
|
|
77
|
+
|
|
78
|
+
// Compare each query token against all text tokens
|
|
79
|
+
for (const qWord of queryTokens) {
|
|
80
|
+
if (textTokens.includes(qWord)) {
|
|
81
|
+
score += 20; // Full match
|
|
82
|
+
} else {
|
|
83
|
+
// Fuzzy match via Levenshtein similarity
|
|
84
|
+
const bestSim = Math.max(...textTokens.map(tw => {
|
|
85
|
+
const dist = levenshtein(tw, qWord);
|
|
86
|
+
return (1 - dist / Math.max(tw.length, qWord.length)) * 100;
|
|
87
|
+
}));
|
|
88
|
+
if (bestSim > 50) score += bestSim / 2; // Half points for close match
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extra points if any word in text starts with a query token
|
|
93
|
+
if (textTokens.some(tw => queryTokens.some(q => tw.startsWith(q)))) score += 10;
|
|
94
|
+
|
|
95
|
+
return Math.min(Math.round(score), 100);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compute weighted score for an item (title is weighted higher than description)
|
|
100
|
+
* @param {object} item
|
|
101
|
+
* @param {string} query
|
|
102
|
+
* @returns {number}
|
|
103
|
+
*/
|
|
104
|
+
const calculateItemScore = (item, query) => {
|
|
105
|
+
const title = item._lcTitle ?? normalize(item.title);
|
|
106
|
+
const desc = item._lcDesc ?? normalize(item.description);
|
|
107
|
+
return matchScore(title, query) * 3 + matchScore(desc, query);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Main search function
|
|
112
|
+
* @param {string} query
|
|
113
|
+
* @param {Array<object>} dataList
|
|
114
|
+
* @param {boolean} enableLog
|
|
115
|
+
* @returns {Array<object>} results sorted by relevance
|
|
116
|
+
*/
|
|
117
|
+
const searchAlgoritm = (query, dataList, enableLog = false) => {
|
|
118
|
+
if (!query || !dataList) return [];
|
|
119
|
+
|
|
120
|
+
const normalizedQuery = normalize(query);
|
|
121
|
+
|
|
122
|
+
// Precompute normalized fields for faster scoring
|
|
123
|
+
dataList.forEach(item => {
|
|
124
|
+
item._lcTitle ??= normalize(item.title);
|
|
125
|
+
item._lcDesc ??= normalize(item.description);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const results = dataList
|
|
129
|
+
.map(item => ({ ...item, _score: calculateItemScore(item, normalizedQuery) }))
|
|
130
|
+
.filter(item => item._score > 0)
|
|
131
|
+
.sort((a, b) => b._score - a._score);
|
|
132
|
+
|
|
133
|
+
if (enableLog) {
|
|
134
|
+
console.log(`[searchAlgoritm] 🔎 Query: "${query}", results: ${results.length}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return results;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
module.exports = { searchAlgoritm };
|