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.
Files changed (3) hide show
  1. package/README.md +35 -8
  2. package/integration.ts +67 -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
 
@@ -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 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.
41
51
 
42
52
  ### Example:
43
53
 
44
54
  ```astro
45
55
  ---
46
- import { find } from "astro-relatinator";
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
- const related = find(id, frontmatter.title, frontmatter.tags, frontmatter.description, body, 3)
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, tfIdf } from "relatinator";
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 { 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(paths: string[], schema: string[], debug = false) {
15
+ const documents: RelatinatorDocument[] = [];
24
16
 
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
- }
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
- let documentContent = '';
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 += ' ' + stringifyData(parsed.data[key]);
40
+ documentContent += " " + stringifyData(parsed.data[key]);
45
41
  }
46
42
  }
47
- documentContent += ' ' + parsed.content;
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
- train(documents);
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("Training TF-IDF on existing data...");
73
- await trainModel(paths, schema);
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 TF-IDF...`
111
+ `Markdown file changed: ${filePath}; Re-training ${similarityMethod}...`
84
112
  );
85
- await trainModel(paths, schema);
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("Training TF-IDF on existing data...");
95
- await trainModel(paths, schema);
96
- logger.info("Training done.");
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": "1.1.0",
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": "^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.0"
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.0"
39
40
  },
40
41
  "peerDependencies": {
41
- "astro": ">=3"
42
+ "astro": ">=5"
42
43
  }
43
- }
44
+ }