astro-relatinator 1.2.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +14 -7
  2. package/integration.ts +88 -35
  3. 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 a single input argument, `paths`, which 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>/`.
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
 
@@ -30,31 +35,33 @@ export default defineConfig({
30
35
  relatinatorIntegration({
31
36
  paths: ["./src/content/posts"], // path yo your content files
32
37
  schema: ["title", "descriptions", "tags"], // frontmatter fields to use for training
38
+ similarityMethod: "bm25", // use BM25 for training
39
+ debug: true, // enable debug mode
33
40
  }),
34
41
  // ...
35
42
  ],
36
43
  });
37
44
  ```
38
45
 
39
- __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.
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.
40
47
 
41
48
  ## Usage
42
49
 
43
- The integration hooks into the Astro build (or dev) process, and will traing the tf-idf model based on the files you've provided. You can then use the `find` function from the package to find related content.
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.
44
51
 
45
52
  ### Example:
46
53
 
47
54
  ```astro
48
55
  ---
49
56
  import { getCollection } from "astro:content";
50
- import { find } from "astro-relatinator";
57
+ import { findRelated } from "astro-relatinator";
51
58
 
52
59
  const blogEntries = await getCollection("posts"); // assuming your collection is called posts
53
60
  // let's say we want to compare against the first blog entry
54
61
  const first = blogEntries[0];
55
62
 
56
63
  // Concatenate the same fields we used for training and declare how many related posts we want to find
57
- const related = find(`${first.frontmatter.title} ${first.frontmatter.description} ${first.frontmatter.tags.join(' ')} ${first.body}`, 3)
64
+ const related = findRelated(`${first.frontmatter.title} ${first.frontmatter.description} ${first.frontmatter.tags.join(' ')} ${first.body}`, first.id, 3)
58
65
 
59
66
  ---
60
67
 
@@ -71,4 +78,4 @@ const related = find(`${first.frontmatter.title} ${first.frontmatter.description
71
78
  }) }
72
79
  </div>
73
80
 
74
- ```
81
+ ```
package/integration.ts CHANGED
@@ -1,50 +1,55 @@
1
1
  import type { AstroIntegration } from "astro";
2
2
  import path from "node:path";
3
- import { train, tfIdf } from "relatinator";
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";
7
- import { TfIdfDocument } from "natural";
8
- import { Document } from "relatinator";
7
+ import { RelatinatorDocument } from "relatinator";
9
8
 
10
- const stringifyData = (data: any): string => {
11
- if (typeof data === 'object') {
12
- if (Array.isArray(data)) {
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: Document[] = [];
14
+ async function trainModel(
15
+ similarityMethod: "tfidf" | "bm25",
16
+ paths: string[],
17
+ schema: string[],
18
+ debug = false
19
+ ) {
20
+ const documents: RelatinatorDocument[] = [];
24
21
 
25
- if (tfIdf.documents) {
26
- if ((tfIdf.documents as unknown as TfIdfDocument[]).length > 0) {
27
- (tfIdf.documents as unknown as TfIdfDocument[]) = [];
28
- (tfIdf.documents as unknown as TfIdfDocument[]).length = 0;
29
- }
22
+ if (debug) {
23
+ await writeDebug("[trainModel] Training TF-IDF on existing data...");
30
24
  }
31
25
 
32
26
  for (const path of paths) {
33
27
  const markdownFiles = await glob(`${path}/**/*.{md,mdx}`);
34
28
 
29
+ if (debug) {
30
+ await writeDebug(
31
+ `[trainModel] Found ${markdownFiles.length} files in ${path}`
32
+ );
33
+ }
34
+
35
35
  for (const file of markdownFiles) {
36
36
  const content = await fs.readFile(file, "utf-8");
37
37
  const parsed = matter(content);
38
- const idArr = file.split("/");
39
- const id = idArr[idArr.length - 1];
40
38
 
41
- let documentContent = '';
39
+ // Use the document's title as the ID
40
+ const id = parsed.data.title;
41
+
42
+ let documentContent = "";
42
43
  for (const key of schema) {
43
44
  if (parsed.data[key]) {
44
- documentContent += ' ' + stringifyData(parsed.data[key]);
45
+ documentContent += " " + stringifyData(parsed.data[key]);
45
46
  }
46
47
  }
47
- documentContent += ' ' + parsed.content;
48
+ documentContent += " " + parsed.content;
49
+
50
+ if (debug) {
51
+ await writeDebug(`[trainModel] Adding document with ID: ${id}`);
52
+ }
48
53
 
49
54
  documents.push({
50
55
  id,
@@ -53,15 +58,44 @@ async function trainModel(paths: string[], schema: string[]) {
53
58
  }
54
59
  }
55
60
 
56
- train(documents);
61
+ if (debug) {
62
+ await writeDebug(
63
+ `[trainModel] Training with documents: ${documents
64
+ .map((d) => d.id)
65
+ .join(", ")}`
66
+ );
67
+ }
68
+
69
+ // Train the existing instance
70
+ if (similarityMethod === "tfidf") {
71
+ TfIdfUtils.train(documents, debug);
72
+ } else {
73
+ BM25Utils.train(documents, debug);
74
+ }
57
75
  }
58
76
 
77
+ const stringifyData = (data: any): string => {
78
+ if (typeof data === "object") {
79
+ if (Array.isArray(data)) {
80
+ return data.map(stringifyData).join(" ");
81
+ } else {
82
+ return Object.values(data).map(stringifyData).join(" ");
83
+ }
84
+ } else {
85
+ return `${data}`;
86
+ }
87
+ };
88
+
59
89
  const relatinatorIntegration = ({
60
90
  paths,
61
91
  schema,
92
+ similarityMethod = "tfidf",
93
+ debug = false,
62
94
  }: {
63
95
  paths: string[];
64
96
  schema: string[];
97
+ similarityMethod?: "tfidf" | "bm25";
98
+ debug?: boolean;
65
99
  }): AstroIntegration => {
66
100
  return {
67
101
  name: "astro-relatinator",
@@ -69,8 +103,15 @@ const relatinatorIntegration = ({
69
103
  "astro:server:setup": async ({ server, logger }) => {
70
104
  logger.info("relatinator server:setup");
71
105
 
72
- logger.info("Training TF-IDF on existing data...");
73
- await trainModel(paths, schema);
106
+ logger.info(
107
+ `Training Relatinator using ${similarityMethod} on existing data...`
108
+ );
109
+ if (similarityMethod === "tfidf") {
110
+ await TfIdfUtils.getInstance();
111
+ } else {
112
+ await BM25Utils.getInstance();
113
+ }
114
+ await trainModel(similarityMethod, paths, schema, debug);
74
115
  logger.info("Initial training done.");
75
116
 
76
117
  server.watcher.on("all", async (eventName, filePath) => {
@@ -80,9 +121,14 @@ const relatinatorIntegration = ({
80
121
 
81
122
  if (path.extname(filePath).includes("md")) {
82
123
  logger.info(
83
- `Markdown file changed: ${filePath}; Re-training TF-IDF...`
124
+ `Markdown file changed: ${filePath}; Re-training ${similarityMethod}...`
84
125
  );
85
- await trainModel(paths, schema);
126
+ if (similarityMethod === "tfidf") {
127
+ await TfIdfUtils.getInstance();
128
+ } else {
129
+ await BM25Utils.getInstance();
130
+ }
131
+ await trainModel(similarityMethod, paths, schema, debug);
86
132
  } else {
87
133
  return;
88
134
  }
@@ -91,13 +137,20 @@ const relatinatorIntegration = ({
91
137
  "astro:build:setup": async ({ logger }) => {
92
138
  logger.info("relatinator build:setup");
93
139
 
94
- logger.info("Training TF-IDF on existing data...");
95
- await trainModel(paths, schema);
96
- logger.info("Training done.");
97
- }
140
+ logger.info(
141
+ `Training Relatinator using ${similarityMethod} on existing data...`
142
+ );
143
+ if (similarityMethod === "tfidf") {
144
+ await TfIdfUtils.getInstance();
145
+ } else {
146
+ await BM25Utils.getInstance();
147
+ }
148
+ await trainModel(similarityMethod, paths, schema, debug);
149
+ logger.info(`Training done.`);
150
+ },
98
151
  },
99
152
  };
100
153
  };
101
154
 
155
+ export { TfIdfUtils, BM25Utils };
102
156
  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": "1.2.0",
3
+ "version": "2.0.1",
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": "^20.10.4",
32
- "astro": ">=3",
33
- "husky": "^8.0.3",
34
- "natural": "^6.10.3"
31
+ "@types/node": "^22.13.4",
32
+ "astro": ">=5",
33
+ "husky": "^9.1.7",
34
+ "vite": "^6.1.1"
35
35
  },
36
36
  "dependencies": {
37
- "glob": "^10.3.10",
38
- "relatinator": "^1.1.0"
37
+ "glob": "^11.0.1",
38
+ "gray-matter": "^4.0.3",
39
+ "relatinator": "2.0.1"
39
40
  },
40
41
  "peerDependencies": {
41
- "astro": ">=3"
42
+ "astro": ">=5"
42
43
  }
43
- }
44
+ }