astro-relatinator 2.0.0 → 2.1.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/README.md +14 -7
- package/integration.ts +51 -16
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -47,35 +47,42 @@ export default defineConfig({
|
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
50
|
-
The integration hooks into the Astro build (or dev) process, and will train the model based on the files you've provided. You can then use the
|
|
50
|
+
The integration hooks into the Astro build (or dev) process, and will train the model based on the files you've provided. You can then use the utility functions exported by the package (`BM25Utils` or `TfIdfUtils`, depending on your chosen `similarityMethod`) to find related content and terms.
|
|
51
51
|
|
|
52
52
|
### Example:
|
|
53
53
|
|
|
54
54
|
```astro
|
|
55
55
|
---
|
|
56
56
|
import { getCollection } from "astro:content";
|
|
57
|
-
|
|
57
|
+
// Import the utils corresponding to your configured similarityMethod
|
|
58
|
+
// (BM25Utils in this example, matching the config example above)
|
|
59
|
+
import { BM25Utils } from "astro-relatinator";
|
|
58
60
|
|
|
59
61
|
const blogEntries = await getCollection("posts"); // assuming your collection is called posts
|
|
60
62
|
// let's say we want to compare against the first blog entry
|
|
61
63
|
const first = blogEntries[0];
|
|
62
64
|
|
|
63
65
|
// Concatenate the same fields we used for training and declare how many related posts we want to find
|
|
64
|
-
const related = findRelated(`${first.
|
|
66
|
+
const related = await BM25Utils.findRelated(`${first.data.title} ${first.data.description} ${first.data.tags.join(' ')} ${first.body}`, first.id, 3)
|
|
65
67
|
|
|
66
68
|
---
|
|
67
69
|
|
|
68
70
|
<div>
|
|
69
71
|
<!-- render your related posts here -->
|
|
70
72
|
{ related.map((id) => {
|
|
71
|
-
const post =
|
|
73
|
+
const post = blogEntries.find((post) => post.id === id);
|
|
74
|
+
if (!post) return null; // Handle case where post might not be found
|
|
72
75
|
return (
|
|
73
|
-
<a href={post.slug}>
|
|
74
|
-
<h2>{post.
|
|
75
|
-
<p>{post.
|
|
76
|
+
<a href={`/posts/${post.slug}/`}> {/* Adjust href based on your routing */}
|
|
77
|
+
<h2>{post.data.title}</h2>
|
|
78
|
+
<p>{post.data.description}</p>
|
|
76
79
|
</a>
|
|
77
80
|
);
|
|
78
81
|
}) }
|
|
79
82
|
</div>
|
|
80
83
|
|
|
81
84
|
```
|
|
85
|
+
|
|
86
|
+
**Note:** The `BM25Utils` and `TfIdfUtils` objects also provide `getTopTermsForId` and `getTopRelatedDocumentsForTerm` functions, similar to the core `relatinator` library.
|
|
87
|
+
|
|
88
|
+
**Development Server:** When running `astro dev`, the integration uses debouncing (a small delay) before re-training the model after a markdown file change. This improves performance for larger sites by preventing rapid, successive re-trains during saves.
|
package/integration.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { TfIdfUtils, BM25Utils } from "relatinator";
|
|
4
4
|
import { glob } from "glob";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
@@ -11,7 +11,12 @@ async function writeDebug(message: string) {
|
|
|
11
11
|
console.log("[astro-relatinator]", message);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
async function trainModel(
|
|
14
|
+
async function trainModel(
|
|
15
|
+
similarityMethod: "tfidf" | "bm25",
|
|
16
|
+
paths: string[],
|
|
17
|
+
schema: string[],
|
|
18
|
+
debug = false
|
|
19
|
+
) {
|
|
15
20
|
const documents: RelatinatorDocument[] = [];
|
|
16
21
|
|
|
17
22
|
if (debug) {
|
|
@@ -62,7 +67,11 @@ async function trainModel(paths: string[], schema: string[], debug = false) {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
// Train the existing instance
|
|
65
|
-
|
|
70
|
+
if (similarityMethod === "tfidf") {
|
|
71
|
+
TfIdfUtils.train(documents, debug);
|
|
72
|
+
} else {
|
|
73
|
+
BM25Utils.train(documents, debug);
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
const stringifyData = (data: any): string => {
|
|
@@ -92,28 +101,50 @@ const relatinatorIntegration = ({
|
|
|
92
101
|
name: "astro-relatinator",
|
|
93
102
|
hooks: {
|
|
94
103
|
"astro:server:setup": async ({ server, logger }) => {
|
|
104
|
+
let debounceTimeout: NodeJS.Timeout | null = null;
|
|
105
|
+
const debounceMs = 500; // Debounce delay in milliseconds
|
|
95
106
|
logger.info("relatinator server:setup");
|
|
96
107
|
|
|
97
108
|
logger.info(
|
|
98
109
|
`Training Relatinator using ${similarityMethod} on existing data...`
|
|
99
110
|
);
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
if (similarityMethod === "tfidf") {
|
|
112
|
+
await TfIdfUtils.getInstance();
|
|
113
|
+
} else {
|
|
114
|
+
await BM25Utils.getInstance();
|
|
115
|
+
}
|
|
116
|
+
await trainModel(similarityMethod, paths, schema, debug);
|
|
102
117
|
logger.info("Initial training done.");
|
|
103
118
|
|
|
104
|
-
server.watcher.on("all",
|
|
119
|
+
server.watcher.on("all", (eventName, filePath) => {
|
|
105
120
|
if (!["add", "change"].includes(eventName)) {
|
|
106
121
|
return;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
if (path.extname(filePath).includes("md")) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
// Clear existing timer if there is one
|
|
126
|
+
if (debounceTimeout) {
|
|
127
|
+
clearTimeout(debounceTimeout);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set a new timer
|
|
131
|
+
debounceTimeout = setTimeout(async () => {
|
|
132
|
+
logger.info(
|
|
133
|
+
`Debounced: Markdown file changed: ${filePath}; Re-training ${similarityMethod}...`
|
|
134
|
+
);
|
|
135
|
+
try {
|
|
136
|
+
// Reset the instance before re-training
|
|
137
|
+
if (similarityMethod === "tfidf") {
|
|
138
|
+
await TfIdfUtils.resetInstance();
|
|
139
|
+
} else {
|
|
140
|
+
await BM25Utils.resetInstance();
|
|
141
|
+
}
|
|
142
|
+
await trainModel(similarityMethod, paths, schema, debug);
|
|
143
|
+
logger.info("Debounced re-training complete.");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error(`Error during debounced re-training: ${error}`);
|
|
146
|
+
}
|
|
147
|
+
}, debounceMs);
|
|
117
148
|
}
|
|
118
149
|
});
|
|
119
150
|
},
|
|
@@ -123,13 +154,17 @@ const relatinatorIntegration = ({
|
|
|
123
154
|
logger.info(
|
|
124
155
|
`Training Relatinator using ${similarityMethod} on existing data...`
|
|
125
156
|
);
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
if (similarityMethod === "tfidf") {
|
|
158
|
+
await TfIdfUtils.getInstance();
|
|
159
|
+
} else {
|
|
160
|
+
await BM25Utils.getInstance();
|
|
161
|
+
}
|
|
162
|
+
await trainModel(similarityMethod, paths, schema, debug);
|
|
128
163
|
logger.info(`Training done.`);
|
|
129
164
|
},
|
|
130
165
|
},
|
|
131
166
|
};
|
|
132
167
|
};
|
|
133
168
|
|
|
134
|
-
export {
|
|
169
|
+
export { TfIdfUtils, BM25Utils };
|
|
135
170
|
export default relatinatorIntegration;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-relatinator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Find related content in your Astro website or app",
|
|
5
5
|
"author": "Darko Bozhinovski <dbozhinovski@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"@types/node": "^22.13.4",
|
|
32
32
|
"astro": ">=5",
|
|
33
33
|
"husky": "^9.1.7",
|
|
34
|
-
"vite": "^6.1.
|
|
34
|
+
"vite": "^6.1.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"glob": "^11.0.1",
|
|
38
38
|
"gray-matter": "^4.0.3",
|
|
39
|
-
"relatinator": "2.
|
|
39
|
+
"relatinator": "2.1.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"astro": ">=5"
|