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.
Files changed (3) hide show
  1. package/README.md +14 -7
  2. package/integration.ts +51 -16
  3. 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 `findRelated` 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 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
- import { findRelated } from "astro-relatinator";
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.frontmatter.title} ${first.frontmatter.description} ${first.frontmatter.tags.join(' ')} ${first.body}`, first.id, 3)
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 = posts.find((post) => post.id === id);
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.frontmatter.title}</h2>
75
- <p>{post.frontmatter.description}</p>
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 { train, getInstance } 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";
@@ -11,7 +11,12 @@ async function writeDebug(message: string) {
11
11
  console.log("[astro-relatinator]", message);
12
12
  }
13
13
 
14
- async function trainModel(paths: string[], schema: string[], debug = false) {
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
- train(documents, debug);
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
- await getInstance(similarityMethod);
101
- await trainModel(paths, schema, debug);
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", async (eventName, filePath) => {
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
- logger.info(
111
- `Markdown file changed: ${filePath}; Re-training ${similarityMethod}...`
112
- );
113
- await getInstance(similarityMethod);
114
- await trainModel(paths, schema, debug);
115
- } else {
116
- return;
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
- await getInstance(similarityMethod);
127
- await trainModel(paths, schema, debug);
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 { findRelated, getInstance } from "relatinator";
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.0.0",
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.0"
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.0.0"
39
+ "relatinator": "2.1.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "astro": ">=5"