jekyll-ai-related 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -1
- data/Gemfile.lock +25 -23
- data/MIGRATE.md +2 -4
- data/README.md +61 -150
- data/Rakefile +7 -0
- data/docs/ARCHITECTURE.md +96 -0
- data/docs/CONFIGURATION.md +81 -0
- data/docs/DEVELOPMENT.md +57 -0
- data/docs/EMBEDDINGS.md +107 -0
- data/docs/GETTING_STARTED.md +73 -0
- data/docs/INTEGRATION.md +58 -0
- data/docs/MIGRATIONS.md +64 -0
- data/docs/README.md +26 -0
- data/docs/SECURITY.md +38 -0
- data/docs/SUPABASE.md +64 -0
- data/docs/TROUBLESHOOTING.md +57 -0
- data/docs/WORKFLOW.md +47 -0
- data/jekyll-ai-related.gemspec +1 -0
- data/lib/jekyll/embeddings-generator/embeddings/fingerprint.rb +18 -0
- data/lib/jekyll/embeddings-generator/embeddings/generate.rb +2 -1
- data/lib/jekyll/embeddings-generator/embeddings/store.rb +65 -64
- data/lib/jekyll/embeddings-generator/models/data.rb +9 -7
- data/lib/jekyll/embeddings-generator/version.rb +1 -1
- data/lib/jekyll/processor.rb +24 -12
- data/sql/supabase/README.md +1 -0
- data/sql/supabase/create.sql +1 -0
- data/sql/supabase/migrations/v0.3.0/001_migrate_add_embedding_fingerprint.sql +7 -0
- data/test/fingerprint_test.rb +28 -0
- data/test/store_cache_test.rb +110 -0
- data/test/test_helper.rb +30 -0
- metadata +33 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '03779eb5ce75f69231a8a7563008c2ebe851bc21ef619763d42e119045830b1d'
|
|
4
|
+
data.tar.gz: 4f28fba97390ff1497dfe5309eec9cee958f9148356bcf5673613522fdd6a1ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91b81a9b34325df10bd2d7b649b5e0237590f2a1211e6e3f65307a1019826c4eb8702f90628d7ad7cc5f2b68f4abe2e531f73dd08e0b24d656ae980bfffd3558
|
|
7
|
+
data.tar.gz: 6e84604c67c77b0e7373f352bc0df6aa320b99ba160e186b3dd8f17ef43c879c8a65ad55c11ffce78b6504b1467448b77bc2ca245d1f63c7a7a79bdc0f882853
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
jekyll-ai-related (0.
|
|
4
|
+
jekyll-ai-related (0.3.0)
|
|
5
5
|
httparty (~> 0.22)
|
|
6
6
|
jekyll (>= 3.7, < 5.0)
|
|
7
7
|
json (~> 2.7)
|
|
@@ -9,33 +9,33 @@ PATH
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
-
addressable (2.
|
|
12
|
+
addressable (2.9.0)
|
|
13
13
|
public_suffix (>= 2.0.2, < 8.0)
|
|
14
14
|
ast (2.4.3)
|
|
15
15
|
base64 (0.3.0)
|
|
16
|
-
bigdecimal (4.
|
|
16
|
+
bigdecimal (4.1.2)
|
|
17
17
|
colorator (1.1.0)
|
|
18
|
-
concurrent-ruby (1.3.
|
|
19
|
-
csv (3.3.
|
|
18
|
+
concurrent-ruby (1.3.8)
|
|
19
|
+
csv (3.3.6)
|
|
20
20
|
em-websocket (0.5.3)
|
|
21
21
|
eventmachine (>= 0.12.9)
|
|
22
22
|
http_parser.rb (~> 0)
|
|
23
23
|
eventmachine (1.2.7)
|
|
24
|
-
ffi (1.17.
|
|
25
|
-
ffi (1.17.
|
|
24
|
+
ffi (1.17.4)
|
|
25
|
+
ffi (1.17.4-x86_64-linux-gnu)
|
|
26
26
|
forwardable-extended (2.6.0)
|
|
27
|
-
google-protobuf (4.
|
|
27
|
+
google-protobuf (4.35.1)
|
|
28
28
|
bigdecimal
|
|
29
|
-
rake (
|
|
30
|
-
google-protobuf (4.
|
|
29
|
+
rake (~> 13.3)
|
|
30
|
+
google-protobuf (4.35.1-x86_64-linux-gnu)
|
|
31
31
|
bigdecimal
|
|
32
|
-
rake (
|
|
32
|
+
rake (~> 13.3)
|
|
33
33
|
http_parser.rb (0.8.1)
|
|
34
34
|
httparty (0.24.2)
|
|
35
35
|
csv
|
|
36
36
|
mini_mime (>= 1.0.0)
|
|
37
37
|
multi_xml (>= 0.5.2)
|
|
38
|
-
i18n (1.
|
|
38
|
+
i18n (1.15.2)
|
|
39
39
|
concurrent-ruby (~> 1.0)
|
|
40
40
|
jekyll (4.4.1)
|
|
41
41
|
addressable (~> 2.4)
|
|
@@ -60,12 +60,12 @@ GEM
|
|
|
60
60
|
sass-embedded (~> 1.75)
|
|
61
61
|
jekyll-watch (2.2.1)
|
|
62
62
|
listen (~> 3.0)
|
|
63
|
-
json (2.
|
|
63
|
+
json (2.21.1)
|
|
64
64
|
kramdown (2.5.2)
|
|
65
65
|
rexml (>= 3.4.4)
|
|
66
66
|
kramdown-parser-gfm (1.1.0)
|
|
67
67
|
kramdown (~> 2.0)
|
|
68
|
-
language_server-protocol (3.17.0.
|
|
68
|
+
language_server-protocol (3.17.0.6)
|
|
69
69
|
liquid (4.0.4)
|
|
70
70
|
listen (3.10.0)
|
|
71
71
|
logger
|
|
@@ -74,23 +74,24 @@ GEM
|
|
|
74
74
|
logger (1.7.0)
|
|
75
75
|
mercenary (0.4.0)
|
|
76
76
|
mini_mime (1.1.5)
|
|
77
|
-
|
|
77
|
+
minitest (5.27.0)
|
|
78
|
+
multi_xml (0.9.1)
|
|
78
79
|
bigdecimal (>= 3.1, < 5)
|
|
79
|
-
parallel (1.
|
|
80
|
-
parser (3.3.
|
|
80
|
+
parallel (1.28.0)
|
|
81
|
+
parser (3.3.12.0)
|
|
81
82
|
ast (~> 2.4.1)
|
|
82
83
|
racc
|
|
83
84
|
pathutil (0.16.2)
|
|
84
85
|
forwardable-extended (~> 2.6)
|
|
85
86
|
prism (1.9.0)
|
|
86
|
-
public_suffix (7.0.
|
|
87
|
+
public_suffix (7.0.5)
|
|
87
88
|
racc (1.8.1)
|
|
88
89
|
rainbow (3.1.1)
|
|
89
|
-
rake (13.
|
|
90
|
+
rake (13.4.2)
|
|
90
91
|
rb-fsevent (0.11.2)
|
|
91
92
|
rb-inotify (0.11.1)
|
|
92
93
|
ffi (~> 1.0)
|
|
93
|
-
regexp_parser (2.
|
|
94
|
+
regexp_parser (2.12.0)
|
|
94
95
|
rexml (3.4.4)
|
|
95
96
|
rouge (4.7.0)
|
|
96
97
|
rubocop (1.57.2)
|
|
@@ -104,7 +105,7 @@ GEM
|
|
|
104
105
|
rubocop-ast (>= 1.28.1, < 2.0)
|
|
105
106
|
ruby-progressbar (~> 1.7)
|
|
106
107
|
unicode-display_width (>= 2.4.0, < 3.0)
|
|
107
|
-
rubocop-ast (1.
|
|
108
|
+
rubocop-ast (1.50.0)
|
|
108
109
|
parser (>= 3.3.7.2)
|
|
109
110
|
prism (~> 1.7)
|
|
110
111
|
rubocop-jekyll (0.14.0)
|
|
@@ -115,10 +116,10 @@ GEM
|
|
|
115
116
|
rubocop-ast (>= 1.31.1, < 2.0)
|
|
116
117
|
ruby-progressbar (1.13.0)
|
|
117
118
|
safe_yaml (1.0.5)
|
|
118
|
-
sass-embedded (1.
|
|
119
|
+
sass-embedded (1.102.0)
|
|
119
120
|
google-protobuf (~> 4.31)
|
|
120
121
|
rake (>= 13)
|
|
121
|
-
sass-embedded (1.
|
|
122
|
+
sass-embedded (1.102.0-x86_64-linux-gnu)
|
|
122
123
|
google-protobuf (~> 4.31)
|
|
123
124
|
terminal-table (3.0.2)
|
|
124
125
|
unicode-display_width (>= 1.1.1, < 3)
|
|
@@ -132,6 +133,7 @@ PLATFORMS
|
|
|
132
133
|
DEPENDENCIES
|
|
133
134
|
bundler (~> 2.6)
|
|
134
135
|
jekyll-ai-related!
|
|
136
|
+
minitest (~> 5.25)
|
|
135
137
|
rake (~> 13.0)
|
|
136
138
|
rubocop-jekyll (~> 0.14)
|
|
137
139
|
|
data/MIGRATE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Migrate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This file is the compatibility entry point for migration guidance. See the complete [migration and rebuild guide](docs/MIGRATIONS.md) for current procedures.
|
|
4
4
|
|
|
5
5
|
## From 0.1.0 to 0.2.0
|
|
6
6
|
|
|
@@ -18,6 +18,4 @@ If you are upgrading from `v0.1.0`, you need drop the data and re-run the plugin
|
|
|
18
18
|
|
|
19
19
|
### Environments support
|
|
20
20
|
|
|
21
|
-
This version also
|
|
22
|
-
|
|
23
|
-
You need to set `JEKYLL_ENV` every time you run the plugin to use this feature. It can be set to anything, like `ci`, `dev`, or `production`. Not setting the env var will make the plugin behave like version `0.1.0`.
|
|
21
|
+
This version also introduced environment-specific table and function names through `JEKYLL_ENV`. See the [environment migration guidance](docs/MIGRATIONS.md#environment-migration) for setup and safety details.
|
data/README.md
CHANGED
|
@@ -4,62 +4,52 @@
|
|
|
4
4
|
[](https://github.com/pirafrank/jekyll-ai-related/releases/latest)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
A [Jekyll](https://jekyllrb.com/)
|
|
7
|
+
A [Jekyll](https://jekyllrb.com/) command plugin that uses OpenAI embeddings and Supabase/pgvector to generate related-post data for your blog.
|
|
8
8
|
|
|
9
|
-
The plugin
|
|
9
|
+
The plugin writes related-post results to `_data`, so normal `jekyll build` runs do not call OpenAI or Supabase. Run `jekyll related` when posts are added or changed, or run it in your CI pipeline.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Quick start
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### Requirements
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
- Ruby `>= 3.2`
|
|
16
|
+
- Jekyll `>= 3.7` and `< 5.0`
|
|
17
|
+
- An OpenAI API key
|
|
18
|
+
- A Supabase project with pgvector available
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
### 1. Create the database objects
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
- Supabase account and API key
|
|
24
|
-
- Supabase URL of the project where the DB is located
|
|
22
|
+
Run [`sql/supabase/create.sql`](sql/supabase/create.sql) in the Supabase SQL editor. This creates the default `page_embeddings` table and `cosine_similarity` function.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
For existing installations, follow the [migration guide](docs/MIGRATIONS.md), especially the v0.3.0 `embedding_fingerprint` column migration. See [Supabase setup](docs/SUPABASE.md) for environment-specific tables and maintenance.
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
### 2. Install the plugin
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Add the gem to the Jekyll site's `Gemfile`:
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
1. Add the plugin to you Jekyll site's `Gemfile` in the `:jekyll_plugins` group:
|
|
35
|
-
|
|
36
|
-
```Gemfile
|
|
30
|
+
```ruby
|
|
37
31
|
group :jekyll_plugins do
|
|
38
|
-
gem
|
|
32
|
+
gem "jekyll-ai-related"
|
|
39
33
|
end
|
|
40
34
|
```
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
Then install it:
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
```sh
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
To use the unreleased `main` branch instead:
|
|
47
43
|
|
|
48
|
-
```
|
|
44
|
+
```ruby
|
|
49
45
|
group :jekyll_plugins do
|
|
50
|
-
gem
|
|
46
|
+
gem "jekyll-ai-related", git: "https://github.com/pirafrank/jekyll-ai-related", branch: "main"
|
|
51
47
|
end
|
|
52
48
|
```
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
### 3. Configure the site
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
bundle update jekyll-ai-related
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Configuration
|
|
61
|
-
|
|
62
|
-
Sample configuration to add to your `_config.yml`:
|
|
52
|
+
Add the plugin configuration to `_config.yml`:
|
|
63
53
|
|
|
64
54
|
```yaml
|
|
65
55
|
jekyll-ai-related:
|
|
@@ -75,153 +65,74 @@ jekyll-ai-related:
|
|
|
75
65
|
db_function: cosine_similarity
|
|
76
66
|
```
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
### Configuration options
|
|
81
|
-
|
|
82
|
-
| Option | Type | Description | Default Value |
|
|
83
|
-
| --- | --- | --- | --- |
|
|
84
|
-
| `post_unique_field` | string | A field that uniquely identifies a post | `slug` |
|
|
85
|
-
| `post_updated_field` | string | A field that indicates when the post was last updated | `date` |
|
|
86
|
-
| `output_path` | string | The subdir inside `_data` where related posts will be stored | `related_posts` |
|
|
87
|
-
| `include_drafts` | boolean | Whether to include drafts in the list of related posts | `false` |
|
|
88
|
-
| `include_future` | boolean | Whether to include future posts in the list of related posts | `false` |
|
|
89
|
-
| `related_posts_limit` | integer | The maximum number of related posts to extract per post | `3` |
|
|
90
|
-
| `related_posts_score_threshold` | float | The minimum similarity score to consider a post related | `0.5` |
|
|
91
|
-
| `precision` | integer | The number of decimal digits to round similarity scores to | `3` |
|
|
92
|
-
| `db_table` | string | The name of the table where the embeddings are stored | `page_embeddings` |
|
|
93
|
-
| `db_function` | string | The name of the function to calculate similarity scores | `cosine_similarity` |
|
|
94
|
-
|
|
95
|
-
> [!NOTE]
|
|
96
|
-
> `related_posts_limit` and `related_posts_score_threshold` are used to filter the list of related posts. The plugin will return the top `related_posts_limit` posts with a similarity score greater than `related_posts_score_threshold`. A post may have 0 or more than `related_posts_limit` related posts, but only the top `related_posts_limit` will be returned, if any.
|
|
97
|
-
|
|
98
|
-
### Advanced configuration
|
|
99
|
-
|
|
100
|
-
You can customize the unique and updated fields, for example if you have a plugin that generates a unique ID for each post, or if you have a custom field that indicates when the post was last updated.
|
|
101
|
-
|
|
102
|
-
Check the [post-metadata.rb](https://github.com/pirafrank/fpiracom/blob/8cc17a5801a73f7c8cbad4cbee099db18389b187/_plugins/post-metadata.rb) plugin of mine for an example. You can download and place it in your `_plugins` folder. It will add metadata to each `post` object and makes it accessible anywhere the `post` object is.
|
|
103
|
-
|
|
104
|
-
If you do so, you could use the following configuration:
|
|
105
|
-
|
|
106
|
-
```yaml
|
|
107
|
-
jekyll-ai-related:
|
|
108
|
-
post_unique_field: uid
|
|
109
|
-
post_updated_field: most_recent_edit
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Usage
|
|
113
|
-
|
|
114
|
-
To run, the plugin needs the OpenAI and Supabase API keys, and Supabase URL. The plugin expects them as environment variables.
|
|
68
|
+
These defaults are optional. Set the required credentials in the environment:
|
|
115
69
|
|
|
116
70
|
```sh
|
|
117
71
|
export OPENAI_API_KEY="your_openai_api_key"
|
|
118
|
-
export SUPABASE_URL="
|
|
72
|
+
export SUPABASE_URL="https://your-project.supabase.co"
|
|
119
73
|
export SUPABASE_KEY="your_supabase_api_key"
|
|
120
74
|
```
|
|
121
75
|
|
|
122
|
-
Never
|
|
76
|
+
Never commit API keys. See the [configuration reference](docs/CONFIGURATION.md) for all settings and custom post fields.
|
|
123
77
|
|
|
124
|
-
|
|
78
|
+
### 4. Generate related posts
|
|
125
79
|
|
|
126
80
|
```sh
|
|
127
81
|
bundle exec jekyll related
|
|
128
82
|
```
|
|
129
83
|
|
|
130
|
-
|
|
131
|
-
> Re-run the plugin whenever you add or update posts to update the list of related posts. Only posts with a `post_updated_field` date greater than the last run will be processed.
|
|
132
|
-
|
|
133
|
-
### Options
|
|
134
|
-
|
|
135
|
-
You can pass options to the plugin to change its behavior. Options are passed after the command name. Not specifying an option is the same as specifying the default value.
|
|
136
|
-
|
|
137
|
-
| Option | Description | Default |
|
|
138
|
-
| --- | --- | --- |
|
|
139
|
-
| `--debug` | Most verbose. Sets log level to Debug. | Jekyll default |
|
|
140
|
-
| `--quiet` | Do not print Info logs. Sets log level to Error. | Jekyll default |
|
|
141
|
-
| `--future` | Generate embeddings and find related posts also for those with a future date. | `false` |
|
|
142
|
-
| `--drafts` | Generate embeddings and find related posts also for drafts. | `false` |
|
|
143
|
-
| `--dry-run` | Do not update the database, do not write related posts to disk. | `false` |
|
|
144
|
-
|
|
145
|
-
Example:
|
|
84
|
+
A useful validation run is:
|
|
146
85
|
|
|
147
86
|
```sh
|
|
148
|
-
bundle exec jekyll related --dry-run --
|
|
87
|
+
bundle exec jekyll related --dry-run --debug
|
|
149
88
|
```
|
|
150
89
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Jekyll AI Related can optionally use the `JEKYLL_ENV` environment variable to determine where is running. This allows you to have separate tables and functions for different environments, for example for development and production.
|
|
154
|
-
|
|
155
|
-
When `JEKYLL_ENV` is set, the plugin will append its value to `db_table` and `db_function` configuration options, separated by a `_` character. If it is not set, the plugin won't append anything. You must set `JEKYLL_ENV` every time you run the plugin to use environments.
|
|
156
|
-
|
|
157
|
-
> [!IMPORTANT]
|
|
158
|
-
> You need to make sure you have the corresponding tables and functions on your Supabase project. Edit the committed SQL script accordingly. They work with no modification when `JEKYLL_ENV` is not set.
|
|
90
|
+
Dry-run mode does not update Supabase or write YAML files, but it may call OpenAI for cache misses. Use a normal run to populate the database and generate output.
|
|
159
91
|
|
|
160
|
-
|
|
92
|
+
Generated data is written under `_data/related_posts/`, for example:
|
|
161
93
|
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
db_table: cool_table_name
|
|
165
|
-
db_function: cool_function_name
|
|
94
|
+
```text
|
|
95
|
+
_data/related_posts/my-post.yml
|
|
166
96
|
```
|
|
167
97
|
|
|
168
|
-
the
|
|
169
|
-
|
|
170
|
-
- `cool_table_name_production` as the database table name,
|
|
171
|
-
- `cool_function_name_production` as the database function name.
|
|
172
|
-
|
|
173
|
-
> [!TIP]
|
|
174
|
-
> Explicitly declare `JEKYLL_ENV=production` to avoid overwriting production data by mistake.
|
|
175
|
-
|
|
176
|
-
## Jekyll integration
|
|
98
|
+
See the [integration guide](docs/INTEGRATION.md) for using the data in Liquid templates.
|
|
177
99
|
|
|
178
|
-
|
|
100
|
+
## Environments
|
|
179
101
|
|
|
180
|
-
|
|
102
|
+
Set `JEKYLL_ENV` to isolate development, staging, and production data:
|
|
181
103
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```html
|
|
185
|
-
{% if site.data.related_posts[page.slug] %}
|
|
186
|
-
<div class="related-posts">
|
|
187
|
-
<h3>Related Posts</h3>
|
|
188
|
-
<ul>
|
|
189
|
-
{% for post in site.data.related_posts[page.slug] %}
|
|
190
|
-
<li>
|
|
191
|
-
<a href="{{ post.url }}">{{ post.title }}</a>
|
|
192
|
-
<br/>{{ post.date | date: "%B %d, %Y" }}
|
|
193
|
-
</li>
|
|
194
|
-
{% endfor %}
|
|
195
|
-
</ul>
|
|
196
|
-
</div>
|
|
197
|
-
{% endif %}
|
|
104
|
+
```sh
|
|
105
|
+
JEKYLL_ENV=production bundle exec jekyll related
|
|
198
106
|
```
|
|
199
107
|
|
|
200
|
-
|
|
201
|
-
> The plugin writes posts in descending order of similarity score, from most similar to least similar. You can change the order by reversing the loop in the snippet above.
|
|
202
|
-
|
|
203
|
-
## Development
|
|
204
|
-
|
|
205
|
-
Clone and run `bundle install` to get started.
|
|
108
|
+
The environment suffix is appended to the configured table and RPC function names, so the corresponding Supabase objects must already exist. See [Supabase setup](docs/SUPABASE.md) for details.
|
|
206
109
|
|
|
207
|
-
|
|
110
|
+
## Documentation
|
|
208
111
|
|
|
209
|
-
|
|
112
|
+
The [documentation index](docs/README.md) groups the complete guides by setup, operation, implementation, and development.
|
|
210
113
|
|
|
211
|
-
|
|
114
|
+
- [Getting started](docs/GETTING_STARTED.md)
|
|
115
|
+
- [Configuration](docs/CONFIGURATION.md)
|
|
116
|
+
- [Migrations](docs/MIGRATIONS.md)
|
|
117
|
+
- [Workflow and operations](docs/WORKFLOW.md)
|
|
118
|
+
- [Troubleshooting](docs/TROUBLESHOOTING.md)
|
|
119
|
+
- [Architecture and embeddings](docs/ARCHITECTURE.md), [embedding details](docs/EMBEDDINGS.md)
|
|
120
|
+
- [Development](docs/DEVELOPMENT.md)
|
|
121
|
+
- [Security](docs/SECURITY.md)
|
|
212
122
|
|
|
213
|
-
##
|
|
214
|
-
|
|
215
|
-
Everyone interacting in the project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/pirafrank/jekyll-ai-related/blob/main/CODE_OF_CONDUCT.md).
|
|
216
|
-
|
|
217
|
-
## Guarantee
|
|
123
|
+
## Development
|
|
218
124
|
|
|
219
|
-
|
|
125
|
+
```sh
|
|
126
|
+
bundle install
|
|
127
|
+
bundle exec rake test
|
|
128
|
+
bundle exec rake lint
|
|
129
|
+
bundle exec rake build
|
|
130
|
+
```
|
|
220
131
|
|
|
221
|
-
|
|
132
|
+
See [Development](docs/DEVELOPMENT.md) for the repository layout, validation workflow, and release process.
|
|
222
133
|
|
|
223
|
-
|
|
134
|
+
## Contributing and license
|
|
224
135
|
|
|
225
|
-
|
|
136
|
+
[Bug reports](https://github.com/pirafrank/jekyll-ai-related/issues) and [pull requests](https://github.com/pirafrank/jekyll-ai-related/pulls) are welcome. The project is available under the [MIT License](https://opensource.org/licenses/MIT).
|
|
226
137
|
|
|
227
|
-
|
|
138
|
+
This plugin uses OpenAI and Supabase services. Users are responsible for complying with their respective terms of service. The plugin is not affiliated with either service.
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rubocop/rake_task"
|
|
5
|
+
require "rake/testtask"
|
|
5
6
|
|
|
6
7
|
RuboCop::RakeTask.new
|
|
7
8
|
|
|
@@ -9,6 +10,12 @@ task :default do
|
|
|
9
10
|
sh "rake -AT"
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
|
14
|
+
t.libs << "lib"
|
|
15
|
+
t.libs << "test"
|
|
16
|
+
t.pattern = "test/**/*_test.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
task lint: :rubocop
|
|
13
20
|
|
|
14
21
|
task :changelog do
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Jekyll AI Related is a Ruby gem that adds the `jekyll related` command to Jekyll. The command reads the site's posts, reuses cached embeddings when possible or generates them through OpenAI, stores vectors and post metadata in Supabase/Postgres, performs a similarity search, and writes the resulting related-post lists into the site's `_data` directory.
|
|
4
|
+
|
|
5
|
+
The generated YAML is deliberately local to the site. A normal `jekyll build` can therefore consume related-post data without calling OpenAI or Supabase; the command is run separately when content changes.
|
|
6
|
+
|
|
7
|
+
## System boundary
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
Jekyll site External services Generated site data
|
|
11
|
+
----------- ------------------ -------------------
|
|
12
|
+
posts + front matter
|
|
13
|
+
|
|
|
14
|
+
v
|
|
15
|
+
`jekyll related` --> OpenAI embeddings API
|
|
16
|
+
| |
|
|
17
|
+
| v
|
|
18
|
+
+------------> Supabase REST API
|
|
19
|
+
(Postgres + pgvector)
|
|
20
|
+
|
|
|
21
|
+
v
|
|
22
|
+
similarity results
|
|
23
|
+
|
|
|
24
|
+
v
|
|
25
|
+
`_data/<path>/*.yml`
|
|
26
|
+
|
|
|
27
|
+
v
|
|
28
|
+
normal Jekyll templates / `site.data`
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The repository contains the gem code under `lib/`, the Supabase bootstrap SQL under `sql/supabase/`, and packaging/release configuration. There are no application controllers, web server, runtime database connections, or test suite in this repository; the plugin runs as a batch command during content maintenance.
|
|
32
|
+
|
|
33
|
+
## Runtime components
|
|
34
|
+
|
|
35
|
+
- `lib/jekyll-ai-related.rb` loads Jekyll and the processor.
|
|
36
|
+
- `lib/jekyll/commands/generator.rb` registers the `related` Jekyll command and translates CLI flags into options.
|
|
37
|
+
- `lib/jekyll/embeddings-generator/init.rb` builds and validates configuration, then creates and reads a Jekyll site. `site.generate` is called so other Jekyll generator plugins can enrich post data before processing.
|
|
38
|
+
- `lib/jekyll/processor.rb` coordinates the two phases: embedding persistence and related-post generation.
|
|
39
|
+
- `embeddings/generate.rb` calls `https://api.openai.com/v1/embeddings` using the `text-embedding-3-small` model.
|
|
40
|
+
- `embeddings/store.rb` communicates with Supabase using its REST/PostgREST endpoints. It handles existence checks, upserts, vector retrieval, and the similarity RPC call.
|
|
41
|
+
- `models/data.rb` creates the database payload. `models/metadata.rb` creates the JSON metadata stored beside each vector.
|
|
42
|
+
- `sql/supabase/create.sql` enables pgvector, creates the table and IVFFlat cosine index, and defines the stored procedure used by the plugin.
|
|
43
|
+
|
|
44
|
+
## Command lifecycle
|
|
45
|
+
|
|
46
|
+
1. Jekyll loads the gem and exposes `bundle exec jekyll related`.
|
|
47
|
+
2. The command combines `_config.yml`, CLI flags, environment variables, and defaults. It validates `OPENAI_API_KEY`, `SUPABASE_URL`, and `SUPABASE_KEY`.
|
|
48
|
+
3. A fresh Jekyll site is built with the configured draft and future-post settings. The site is read and generators are run.
|
|
49
|
+
4. For every included post, the plugin extracts `post.content` and metadata, computes a model/content fingerprint, and fetches the existing Supabase record.
|
|
50
|
+
5. The plugin reuses the stored embedding when the fingerprint and vector are present; otherwise it calls OpenAI. It upserts when the row is missing, the fingerprint changed, or the configured update value is newer.
|
|
51
|
+
6. For every included post, it fetches that post's stored embedding and submits a SQL query through the configured Supabase RPC function. The query excludes the current post, applies the score threshold, sorts by cosine distance, and limits the result count.
|
|
52
|
+
7. Non-empty results are serialized as YAML to `_data/<output_path>/<safe-uid>.yml`. Existing files for the same post are overwritten; an empty result does not remove an older file.
|
|
53
|
+
|
|
54
|
+
The two phases are sequential. If related-post lookup fails for one post, the error is logged and processing continues with the next post.
|
|
55
|
+
|
|
56
|
+
## Configuration and naming
|
|
57
|
+
|
|
58
|
+
The `jekyll-ai-related` section of `_config.yml` supports:
|
|
59
|
+
|
|
60
|
+
| Setting | Default | Role |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `post_unique_field` | `slug` | Post field used as the database `uid` and output filename key |
|
|
63
|
+
| `post_updated_field` | `date` | Post field used for update comparison |
|
|
64
|
+
| `output_path` | `related_posts` | Subdirectory under `_data` |
|
|
65
|
+
| `include_drafts` | `false` | Include drafts in the generated Jekyll site |
|
|
66
|
+
| `include_future` | `false` | Include future-dated posts |
|
|
67
|
+
| `related_posts_limit` | `3` | Maximum result count per post |
|
|
68
|
+
| `related_posts_score_threshold` | `0.5` | Minimum cosine similarity |
|
|
69
|
+
| `precision` | `3` | Decimal places used for returned similarity values |
|
|
70
|
+
| `db_table` | `page_embeddings` | Supabase table name |
|
|
71
|
+
| `db_function` | `cosine_similarity` | Supabase RPC function name |
|
|
72
|
+
|
|
73
|
+
`JEKYLL_ENV`, when present, is appended to both database names with an underscore. For example, `JEKYLL_ENV=production` changes the defaults to `page_embeddings_production` and `cosine_similarity_production`. The corresponding objects must exist in Supabase; the plugin does not create environment-specific objects automatically.
|
|
74
|
+
|
|
75
|
+
CLI flags can enable drafts, future posts, and dry-run mode for a single invocation. `--debug` and `--quiet` adjust Jekyll logging.
|
|
76
|
+
|
|
77
|
+
## Output contract
|
|
78
|
+
|
|
79
|
+
For the default configuration, a post with slug `my-post` produces:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
_data/related_posts/my-post.yml
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The YAML value is an array of result objects returned by Supabase. Each result contains `title`, `uid`, `most_recent_edit`, `url`, `date`, and a rounded `similarity` value. Templates access it as `site.data.related_posts[page.slug]`.
|
|
86
|
+
|
|
87
|
+
The filename is lowercased and every character outside `[a-z0-9-_]` is replaced by `-`. The database lookup still uses the original configured post value, so the filename is only a filesystem-safe representation.
|
|
88
|
+
|
|
89
|
+
## Operational characteristics
|
|
90
|
+
|
|
91
|
+
- API keys are read only from environment variables and are sent in the OpenAI and Supabase HTTP headers.
|
|
92
|
+
- The command is not part of the normal build path. Keep generated `_data` files available to the site build, and schedule `jekyll related` after creating or editing posts.
|
|
93
|
+
- OpenAI is called only for cache misses: new posts, changed rendered content, legacy rows without a fingerprint, or after an embedding model change. Supabase reads and similarity searches still run for every included post.
|
|
94
|
+
- `--dry-run` still reads from both services and may generate embeddings for cache misses; it skips Supabase updates and local file writes. It is therefore useful for exercising the flow, but it is not an offline mode.
|
|
95
|
+
- The included SQL uses an IVFFlat index with `vector_cosine_ops` and `lists = 100`. The vector dimension must remain compatible with the selected OpenAI model and the table definition.
|
|
96
|
+
- The plugin uses raw SQL text assembled in Ruby and executes it through a `SECURITY DEFINER` PostgreSQL function. Table/function names and the SQL setup should be treated as trusted deployment configuration.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
Configuration is read from the `jekyll-ai-related` section of `_config.yml`.
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
jekyll-ai-related:
|
|
7
|
+
post_unique_field: slug
|
|
8
|
+
post_updated_field: date
|
|
9
|
+
output_path: related_posts
|
|
10
|
+
include_drafts: false
|
|
11
|
+
include_future: false
|
|
12
|
+
related_posts_limit: 3
|
|
13
|
+
related_posts_score_threshold: 0.5
|
|
14
|
+
precision: 3
|
|
15
|
+
db_table: page_embeddings
|
|
16
|
+
db_function: cosine_similarity
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Setting | Type | Default | Description |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| `post_unique_field` | string | `slug` | Post field used as the database `uid` and output filename key. |
|
|
22
|
+
| `post_updated_field` | string | `date` | Post field compared with the stored `most_recent_edit`. |
|
|
23
|
+
| `output_path` | string | `related_posts` | Directory under `_data` for generated YAML. |
|
|
24
|
+
| `include_drafts` | boolean | `false` | Include drafts when building the processing site. |
|
|
25
|
+
| `include_future` | boolean | `false` | Include future-dated posts. |
|
|
26
|
+
| `related_posts_limit` | integer | `3` | Maximum number of results per source post. |
|
|
27
|
+
| `related_posts_score_threshold` | number | `0.5` | Minimum cosine similarity. |
|
|
28
|
+
| `precision` | integer | `3` | Decimal places used in returned similarity values. |
|
|
29
|
+
| `db_table` | string | `page_embeddings` | Supabase table containing vectors. |
|
|
30
|
+
| `db_function` | string | `cosine_similarity` | Supabase RPC function used for the search. |
|
|
31
|
+
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> `related_posts_limit` and `related_posts_score_threshold` work together: the plugin returns up to `related_posts_limit` posts whose similarity is greater than `related_posts_score_threshold`. A post can have no qualifying related posts, and the threshold can result in fewer results than the limit.
|
|
34
|
+
|
|
35
|
+
CLI flags are invocation-level overrides for drafts, future posts, and dry run:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
--drafts include drafts
|
|
39
|
+
--future include future posts
|
|
40
|
+
--dry-run skip database updates and file writes
|
|
41
|
+
--debug enable debug logging
|
|
42
|
+
--quiet suppress info logging
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For drafts and future posts, a true value in `_config.yml` or the corresponding CLI flag enables inclusion. `--dry-run` is not an offline mode.
|
|
46
|
+
|
|
47
|
+
## Environment naming
|
|
48
|
+
|
|
49
|
+
If `JEKYLL_ENV` is set, its value is appended to both database names:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
JEKYLL_ENV=production
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With the defaults, the plugin uses `page_embeddings_production` and `cosine_similarity_production`. The suffixed objects must already exist.
|
|
56
|
+
|
|
57
|
+
## Credentials
|
|
58
|
+
|
|
59
|
+
Credentials are not Jekyll configuration values. They are required environment variables:
|
|
60
|
+
|
|
61
|
+
- `OPENAI_API_KEY`
|
|
62
|
+
- `SUPABASE_URL`
|
|
63
|
+
- `SUPABASE_KEY`
|
|
64
|
+
|
|
65
|
+
Missing credentials stop configuration validation before processing posts.
|
|
66
|
+
|
|
67
|
+
## Precedence and important behavior
|
|
68
|
+
|
|
69
|
+
Configuration values come from `_config.yml` and defaults; CLI flags can enable drafts, future posts, and dry run for the current command. `post_unique_field` and `post_updated_field` must exist on every processed post. The plugin computes a content fingerprint before calling OpenAI and reuses stored vectors when the fingerprint matches. Metadata-only changes still trigger a database upsert when the configured update timestamp is newer than the stored value.
|
|
70
|
+
|
|
71
|
+
## Advanced custom fields
|
|
72
|
+
|
|
73
|
+
Sites can use custom post metadata for identity and freshness. For example, a plugin that adds a unique `uid` and a `most_recent_edit` field can use:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
jekyll-ai-related:
|
|
77
|
+
post_unique_field: uid
|
|
78
|
+
post_updated_field: most_recent_edit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The fields must be available on the post after Jekyll's generators have run. `uid` must be unique across the configured Supabase table, and `most_recent_edit` must be comparable with the stored timestamp.
|