astro-relatinator 1.1.0 → 2.0.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 +35 -8
- package/integration.ts +67 -35
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Assuming you're running Astro v3 or later, you can install this integration by r
|
|
|
10
10
|
npx astro add astro-relatinator
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
OR
|
|
13
|
+
OR
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
npm install astro-relatinator
|
|
@@ -18,7 +18,12 @@ npm install astro-relatinator
|
|
|
18
18
|
|
|
19
19
|
## Configuration
|
|
20
20
|
|
|
21
|
-
The integration takes
|
|
21
|
+
The integration takes four input arguments, `paths`, `schema`, `similarityMethod`, and `debug`.
|
|
22
|
+
|
|
23
|
+
- `paths` is an array of paths to the `.md` or `.mdx` files you want to use for training the model. If you're using the [Content Collections API](https://docs.astro.build/en/guides/content-collections/), you can find these at `./src/collections/<collection-name>/`.
|
|
24
|
+
- `schema` is an array of frontmatter fields you want to use for training the model.
|
|
25
|
+
- `similarityMethod` is the algorithm to use for training the model. Can be `"tfidf"` or `"bm25"`.
|
|
26
|
+
- `debug` is a boolean flag to enable debug mode. It gets VERY verbose, so use it with caution.
|
|
22
27
|
|
|
23
28
|
### Example:
|
|
24
29
|
|
|
@@ -28,27 +33,49 @@ export default defineConfig({
|
|
|
28
33
|
integrations: [
|
|
29
34
|
// ...
|
|
30
35
|
relatinatorIntegration({
|
|
31
|
-
paths: ["./src/content/posts"],
|
|
36
|
+
paths: ["./src/content/posts"], // path yo your content files
|
|
37
|
+
schema: ["title", "descriptions", "tags"], // frontmatter fields to use for training
|
|
38
|
+
similarityMethod: "bm25", // use BM25 for training
|
|
39
|
+
debug: true, // enable debug mode
|
|
32
40
|
}),
|
|
33
41
|
// ...
|
|
34
42
|
],
|
|
35
43
|
});
|
|
36
44
|
```
|
|
37
45
|
|
|
46
|
+
**Note:** the fields you specify in the `schema` array must be present in the frontmatter of the files you're training on. If they're not, the integration will throw an error or lead to unexpected results.
|
|
47
|
+
|
|
38
48
|
## Usage
|
|
39
49
|
|
|
40
|
-
The integration hooks into the Astro build (or dev) process, and will
|
|
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 `findRelated` function from the package to find related content.
|
|
41
51
|
|
|
42
52
|
### Example:
|
|
43
53
|
|
|
44
54
|
```astro
|
|
45
55
|
---
|
|
46
|
-
import {
|
|
56
|
+
import { getCollection } from "astro:content";
|
|
57
|
+
import { findRelated } from "astro-relatinator";
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
const blogEntries = await getCollection("posts"); // assuming your collection is called posts
|
|
60
|
+
// let's say we want to compare against the first blog entry
|
|
61
|
+
const first = blogEntries[0];
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
// Concatenate the same fields we used for training and declare how many related posts we want to find
|
|
64
|
+
const related = findRelated(`${first.frontmatter.title} ${first.frontmatter.description} ${first.frontmatter.tags.join(' ')} ${first.body}`, first.id, 3)
|
|
51
65
|
|
|
52
66
|
---
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
<div>
|
|
69
|
+
<!-- render your related posts here -->
|
|
70
|
+
{ related.map((id) => {
|
|
71
|
+
const post = posts.find((post) => post.id === id);
|
|
72
|
+
return (
|
|
73
|
+
<a href={post.slug}>
|
|
74
|
+
<h2>{post.frontmatter.title}</h2>
|
|
75
|
+
<p>{post.frontmatter.description}</p>
|
|
76
|
+
</a>
|
|
77
|
+
);
|
|
78
|
+
}) }
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
```
|
package/integration.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { train,
|
|
3
|
+
import { train, getInstance } from "relatinator";
|
|
4
4
|
import { glob } from "glob";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
|
-
import {
|
|
8
|
-
import { Document } from "relatinator";
|
|
7
|
+
import { RelatinatorDocument } from "relatinator";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return data.map(stringifyData).join(' ');
|
|
14
|
-
} else {
|
|
15
|
-
return Object.values(data).map(stringifyData).join(' ');
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
return String(data);
|
|
19
|
-
}
|
|
9
|
+
// Helper to write debug info to a file
|
|
10
|
+
async function writeDebug(message: string) {
|
|
11
|
+
console.log("[astro-relatinator]", message);
|
|
20
12
|
}
|
|
21
13
|
|
|
22
|
-
async function trainModel(paths: string[], schema: string[]) {
|
|
23
|
-
const documents:
|
|
14
|
+
async function trainModel(paths: string[], schema: string[], debug = false) {
|
|
15
|
+
const documents: RelatinatorDocument[] = [];
|
|
24
16
|
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
(tfIdf.documents as unknown as TfIdfDocument[]) = [];
|
|
28
|
-
(tfIdf.documents as unknown as TfIdfDocument[]).length = 0;
|
|
29
|
-
}
|
|
17
|
+
if (debug) {
|
|
18
|
+
await writeDebug("[trainModel] Training TF-IDF on existing data...");
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
for (const path of paths) {
|
|
33
22
|
const markdownFiles = await glob(`${path}/**/*.{md,mdx}`);
|
|
34
23
|
|
|
24
|
+
if (debug) {
|
|
25
|
+
await writeDebug(
|
|
26
|
+
`[trainModel] Found ${markdownFiles.length} files in ${path}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
35
30
|
for (const file of markdownFiles) {
|
|
36
31
|
const content = await fs.readFile(file, "utf-8");
|
|
37
32
|
const parsed = matter(content);
|
|
38
|
-
const idArr = file.split("/");
|
|
39
|
-
const id = idArr[idArr.length - 1];
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
// Use the document's title as the ID
|
|
35
|
+
const id = parsed.data.title;
|
|
36
|
+
|
|
37
|
+
let documentContent = "";
|
|
42
38
|
for (const key of schema) {
|
|
43
39
|
if (parsed.data[key]) {
|
|
44
|
-
documentContent +=
|
|
40
|
+
documentContent += " " + stringifyData(parsed.data[key]);
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
|
-
documentContent +=
|
|
43
|
+
documentContent += " " + parsed.content;
|
|
44
|
+
|
|
45
|
+
if (debug) {
|
|
46
|
+
await writeDebug(`[trainModel] Adding document with ID: ${id}`);
|
|
47
|
+
}
|
|
48
48
|
|
|
49
49
|
documents.push({
|
|
50
50
|
id,
|
|
@@ -53,15 +53,40 @@ async function trainModel(paths: string[], schema: string[]) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
if (debug) {
|
|
57
|
+
await writeDebug(
|
|
58
|
+
`[trainModel] Training with documents: ${documents
|
|
59
|
+
.map((d) => d.id)
|
|
60
|
+
.join(", ")}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Train the existing instance
|
|
65
|
+
train(documents, debug);
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
const stringifyData = (data: any): string => {
|
|
69
|
+
if (typeof data === "object") {
|
|
70
|
+
if (Array.isArray(data)) {
|
|
71
|
+
return data.map(stringifyData).join(" ");
|
|
72
|
+
} else {
|
|
73
|
+
return Object.values(data).map(stringifyData).join(" ");
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
return `${data}`;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
59
80
|
const relatinatorIntegration = ({
|
|
60
81
|
paths,
|
|
61
82
|
schema,
|
|
83
|
+
similarityMethod = "tfidf",
|
|
84
|
+
debug = false,
|
|
62
85
|
}: {
|
|
63
86
|
paths: string[];
|
|
64
87
|
schema: string[];
|
|
88
|
+
similarityMethod?: "tfidf" | "bm25";
|
|
89
|
+
debug?: boolean;
|
|
65
90
|
}): AstroIntegration => {
|
|
66
91
|
return {
|
|
67
92
|
name: "astro-relatinator",
|
|
@@ -69,8 +94,11 @@ const relatinatorIntegration = ({
|
|
|
69
94
|
"astro:server:setup": async ({ server, logger }) => {
|
|
70
95
|
logger.info("relatinator server:setup");
|
|
71
96
|
|
|
72
|
-
logger.info(
|
|
73
|
-
|
|
97
|
+
logger.info(
|
|
98
|
+
`Training Relatinator using ${similarityMethod} on existing data...`
|
|
99
|
+
);
|
|
100
|
+
await getInstance(similarityMethod);
|
|
101
|
+
await trainModel(paths, schema, debug);
|
|
74
102
|
logger.info("Initial training done.");
|
|
75
103
|
|
|
76
104
|
server.watcher.on("all", async (eventName, filePath) => {
|
|
@@ -80,9 +108,10 @@ const relatinatorIntegration = ({
|
|
|
80
108
|
|
|
81
109
|
if (path.extname(filePath).includes("md")) {
|
|
82
110
|
logger.info(
|
|
83
|
-
`Markdown file changed: ${filePath}; Re-training
|
|
111
|
+
`Markdown file changed: ${filePath}; Re-training ${similarityMethod}...`
|
|
84
112
|
);
|
|
85
|
-
await
|
|
113
|
+
await getInstance(similarityMethod);
|
|
114
|
+
await trainModel(paths, schema, debug);
|
|
86
115
|
} else {
|
|
87
116
|
return;
|
|
88
117
|
}
|
|
@@ -91,13 +120,16 @@ const relatinatorIntegration = ({
|
|
|
91
120
|
"astro:build:setup": async ({ logger }) => {
|
|
92
121
|
logger.info("relatinator build:setup");
|
|
93
122
|
|
|
94
|
-
logger.info(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
logger.info(
|
|
124
|
+
`Training Relatinator using ${similarityMethod} on existing data...`
|
|
125
|
+
);
|
|
126
|
+
await getInstance(similarityMethod);
|
|
127
|
+
await trainModel(paths, schema, debug);
|
|
128
|
+
logger.info(`Training done.`);
|
|
129
|
+
},
|
|
98
130
|
},
|
|
99
131
|
};
|
|
100
132
|
};
|
|
101
133
|
|
|
134
|
+
export { findRelated, getInstance } from "relatinator";
|
|
102
135
|
export default relatinatorIntegration;
|
|
103
|
-
export { findRelated, tfIdf } from "relatinator";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-relatinator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -28,16 +28,17 @@
|
|
|
28
28
|
},
|
|
29
29
|
"type": "module",
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
32
|
-
"astro": ">=
|
|
33
|
-
"husky": "^
|
|
34
|
-
"
|
|
31
|
+
"@types/node": "^22.13.4",
|
|
32
|
+
"astro": ">=5",
|
|
33
|
+
"husky": "^9.1.7",
|
|
34
|
+
"vite": "^6.1.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"glob": "^
|
|
38
|
-
"
|
|
37
|
+
"glob": "^11.0.1",
|
|
38
|
+
"gray-matter": "^4.0.3",
|
|
39
|
+
"relatinator": "2.0.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
|
-
"astro": ">=
|
|
42
|
+
"astro": ">=5"
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|