hexo-semantic-search-ai 1.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 ADDED
@@ -0,0 +1,201 @@
1
+ # hexo-semantic-search-ai [中文](README.zh-CN.md)
2
+
3
+ A Hexo plugin that integrates [SemanticSearch](https://github.com/SemanticSearch-ai/semanticsearch) for AI-powered semantic search and related posts.
4
+
5
+ ## Features
6
+
7
+ - **Automatic Indexing**: Syncs posts to SemanticSearch after `hexo generate`
8
+ - **Incremental Sync**: Only syncs changed posts (tracks content hash)
9
+ - **Related Posts**: Generates related posts at build time using semantic similarity
10
+ - **Search Component**: Provides helpers for frontend search UI
11
+ - **Customizable**: Full control over styling and rendering
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install hexo-semantic-search-ai --save
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ You need a SemanticSearch instance. Deploy one for free on Cloudflare Workers:
22
+
23
+ 1. Go to [SemanticSearch](https://github.com/SemanticSearch-ai/semanticsearch)
24
+ 2. Click "Deploy to Cloudflare"
25
+ 3. Get your API endpoint and keys
26
+
27
+ ## Configuration
28
+
29
+ Add to your Hexo `_config.yml`:
30
+
31
+ ```yaml
32
+ semantic_search:
33
+ enable: true
34
+ endpoint: https://your-search.your-subdomain.workers.dev
35
+ writer_key: ${SEMANTIC_SEARCH_WRITER_KEY} # Use env var for security
36
+ reader_key: your-reader-key # Public, safe to expose
37
+
38
+ # Sync settings
39
+ sync:
40
+ auto: true # Auto-sync after hexo generate
41
+ fields: # Fields to index
42
+ - title
43
+ - content
44
+ - excerpt
45
+ - tags
46
+ - categories
47
+
48
+ # Related posts settings
49
+ related_posts:
50
+ enable: true
51
+ limit: 5 # Max related posts per article
52
+ min_score: 0.3 # Minimum similarity score (0-1)
53
+ query_fields: # Fields used to find related posts
54
+ - title
55
+ - excerpt
56
+
57
+ # Search UI settings
58
+ search:
59
+ placeholder: "Search..."
60
+ ```
61
+
62
+ ### Environment Variables
63
+
64
+ For security, use environment variables for your writer key:
65
+
66
+ ```bash
67
+ export SEMANTIC_SEARCH_WRITER_KEY=your-writer-key
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ ### Search Box
73
+
74
+ Add a search box to your theme:
75
+
76
+ ```ejs
77
+ <%- semantic_search_box() %>
78
+ ```
79
+
80
+ With options:
81
+
82
+ ```ejs
83
+ <%- semantic_search_box({
84
+ placeholder: 'Search articles...',
85
+ class: 'my-search-box',
86
+ id: 'custom-search'
87
+ }) %>
88
+ ```
89
+
90
+ Don't forget to include the JS file:
91
+
92
+ ```ejs
93
+ <script src="<%- url_for('/js/semantic-search.js') %>"></script>
94
+ ```
95
+
96
+ ### Related Posts
97
+
98
+ Display related posts in your post template:
99
+
100
+ ```ejs
101
+ <%- semantic_related_posts() %>
102
+ ```
103
+
104
+ With options:
105
+
106
+ ```ejs
107
+ <%- semantic_related_posts({
108
+ limit: 3,
109
+ title: 'You might also like',
110
+ class: 'related-articles',
111
+ excerpt: false
112
+ }) %>
113
+ ```
114
+
115
+ ### Custom Rendering
116
+
117
+ For full control, access the raw data:
118
+
119
+ ```ejs
120
+ <% if (has_semantic_related()) { %>
121
+ <div class="my-related-posts">
122
+ <h3>Related</h3>
123
+ <% get_semantic_related().forEach(function(post) { %>
124
+ <article>
125
+ <a href="<%= post.url %>"><%= post.title %></a>
126
+ <p><%= post.excerpt %></p>
127
+ </article>
128
+ <% }); %>
129
+ </div>
130
+ <% } %>
131
+ ```
132
+
133
+ ### Frontend JavaScript API
134
+
135
+ For advanced use cases, use the JavaScript API directly:
136
+
137
+ ```html
138
+ <script src="/js/semantic-search.js"></script>
139
+ <script>
140
+ // Configure (if not using helper)
141
+ SemanticSearch.configure({
142
+ endpoint: 'https://your-search.workers.dev',
143
+ readerKey: 'your-reader-key'
144
+ });
145
+
146
+ // Search
147
+ SemanticSearch.search('how to deploy').then(results => {
148
+ console.log(results);
149
+ });
150
+
151
+ // Or bind to an input
152
+ SemanticSearch.bindSearchBox('#search-input', '#search-results', {
153
+ limit: 10,
154
+ debounce: 300,
155
+ minLength: 2
156
+ });
157
+ </script>
158
+ ```
159
+
160
+ ## Commands
161
+
162
+ ### Sync manually
163
+
164
+ ```bash
165
+ # Incremental sync (only changed posts)
166
+ hexo semantic-search sync
167
+
168
+ # Force full sync
169
+ hexo semantic-search sync --force
170
+ # or
171
+ hexo semantic-search full-sync
172
+
173
+ # Check sync status
174
+ hexo semantic-search status
175
+ ```
176
+
177
+ ## State File
178
+
179
+ The plugin creates `.semantic-search-state.json` in your Hexo root to track synced posts. Add it to `.gitignore` if you don't want to commit it:
180
+
181
+ ```
182
+ .semantic-search-state.json
183
+ ```
184
+
185
+ ## Helpers Reference
186
+
187
+ | Helper | Description |
188
+ |--------|-------------|
189
+ | `semantic_search_box(options)` | Render search input with results container |
190
+ | `semantic_search_config()` | Output config JSON for custom frontend |
191
+ | `semantic_related_posts(options)` | Render related posts list |
192
+ | `has_semantic_related()` | Check if current post has related posts |
193
+ | `get_semantic_related()` | Get raw related posts array |
194
+
195
+ ## License
196
+
197
+ MIT
198
+
199
+ ## Friendly Request
200
+
201
+ Friendly request (not required): If you use this plugin, please consider adding a link to https://semanticsearch.ai/ on your website.
@@ -0,0 +1,201 @@
1
+ # hexo-semantic-search-ai [English](README.md)
2
+
3
+ 一个 Hexo 插件,集成 [SemanticSearch](https://github.com/SemanticSearch-ai/semanticsearch),为静态博客提供 AI 语义搜索与相关文章推荐。
4
+
5
+ ## 功能特性
6
+
7
+ - **自动索引**:在 `hexo generate` 后同步文章到 SemanticSearch
8
+ - **增量同步**:仅同步变更文章(跟踪内容哈希)
9
+ - **相关文章**:构建时基于语义相似度生成相关文章
10
+ - **搜索组件**:提供前端搜索 UI 的 helper
11
+ - **可定制**:完全可控的样式与渲染
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install hexo-semantic-search-ai --save
17
+ ```
18
+
19
+ ## 前置条件
20
+
21
+ 你需要一个 SemanticSearch 实例。可以在 Cloudflare Workers 上免费部署:
22
+
23
+ 1. 访问 [SemanticSearch](https://github.com/SemanticSearch-ai/semanticsearch)
24
+ 2. 点击 “Deploy to Cloudflare”
25
+ 3. 获取 API Endpoint 和密钥
26
+
27
+ ## 配置
28
+
29
+ 在 Hexo `_config.yml` 中添加:
30
+
31
+ ```yaml
32
+ semantic_search:
33
+ enable: true
34
+ endpoint: https://your-search.your-subdomain.workers.dev
35
+ writer_key: ${SEMANTIC_SEARCH_WRITER_KEY} # 为了安全使用环境变量
36
+ reader_key: your-reader-key # 公钥,可公开
37
+
38
+ # 同步设置
39
+ sync:
40
+ auto: true # 在 hexo generate 后自动同步
41
+ fields: # 需要索引的字段
42
+ - title
43
+ - content
44
+ - excerpt
45
+ - tags
46
+ - categories
47
+
48
+ # 相关文章设置
49
+ related_posts:
50
+ enable: true
51
+ limit: 5 # 每篇文章最多相关条目
52
+ min_score: 0.3 # 相似度阈值 (0-1)
53
+ query_fields: # 用于检索相关文章的字段
54
+ - title
55
+ - excerpt
56
+
57
+ # 搜索 UI 设置
58
+ search:
59
+ placeholder: "搜索..."
60
+ ```
61
+
62
+ ### 环境变量
63
+
64
+ 为了安全,建议使用环境变量保存 writer key:
65
+
66
+ ```bash
67
+ export SEMANTIC_SEARCH_WRITER_KEY=your-writer-key
68
+ ```
69
+
70
+ ## 使用
71
+
72
+ ### 搜索框
73
+
74
+ 在主题中添加搜索框:
75
+
76
+ ```ejs
77
+ <%- semantic_search_box() %>
78
+ ```
79
+
80
+ 可选参数:
81
+
82
+ ```ejs
83
+ <%- semantic_search_box({
84
+ placeholder: '搜索文章...',
85
+ class: 'my-search-box',
86
+ id: 'custom-search'
87
+ }) %>
88
+ ```
89
+
90
+ 别忘了引入 JS 文件:
91
+
92
+ ```ejs
93
+ <script src="<%- url_for('/js/semantic-search.js') %>"></script>
94
+ ```
95
+
96
+ ### 相关文章
97
+
98
+ 在文章模板中显示相关文章:
99
+
100
+ ```ejs
101
+ <%- semantic_related_posts() %>
102
+ ```
103
+
104
+ 可选参数:
105
+
106
+ ```ejs
107
+ <%- semantic_related_posts({
108
+ limit: 3,
109
+ title: '你可能也喜欢',
110
+ class: 'related-articles',
111
+ excerpt: false
112
+ }) %>
113
+ ```
114
+
115
+ ### 自定义渲染
116
+
117
+ 如需完全控制,可直接获取原始数据:
118
+
119
+ ```ejs
120
+ <% if (has_semantic_related()) { %>
121
+ <div class="my-related-posts">
122
+ <h3>相关内容</h3>
123
+ <% get_semantic_related().forEach(function(post) { %>
124
+ <article>
125
+ <a href="<%= post.url %>"><%= post.title %></a>
126
+ <p><%= post.excerpt %></p>
127
+ </article>
128
+ <% }); %>
129
+ </div>
130
+ <% } %>
131
+ ```
132
+
133
+ ### 前端 JavaScript API
134
+
135
+ 高级用法可直接使用 JavaScript API:
136
+
137
+ ```html
138
+ <script src="/js/semantic-search.js"></script>
139
+ <script>
140
+ // 配置(如果不使用 helper)
141
+ SemanticSearch.configure({
142
+ endpoint: 'https://your-search.workers.dev',
143
+ readerKey: 'your-reader-key'
144
+ });
145
+
146
+ // 搜索
147
+ SemanticSearch.search('how to deploy').then(results => {
148
+ console.log(results);
149
+ });
150
+
151
+ // 或绑定到输入框
152
+ SemanticSearch.bindSearchBox('#search-input', '#search-results', {
153
+ limit: 10,
154
+ debounce: 300,
155
+ minLength: 2
156
+ });
157
+ </script>
158
+ ```
159
+
160
+ ## 命令
161
+
162
+ ### 手动同步
163
+
164
+ ```bash
165
+ # 增量同步(仅同步变更文章)
166
+ hexo semantic-search sync
167
+
168
+ # 强制全量同步
169
+ hexo semantic-search sync --force
170
+ # 或
171
+ hexo semantic-search full-sync
172
+
173
+ # 查看同步状态
174
+ hexo semantic-search status
175
+ ```
176
+
177
+ ## 状态文件
178
+
179
+ 插件会在 Hexo 根目录创建 `.semantic-search-state.json` 用于追踪已同步的文章。如果不想提交,请加入 `.gitignore`:
180
+
181
+ ```
182
+ .semantic-search-state.json
183
+ ```
184
+
185
+ ## Helper 参考
186
+
187
+ | Helper | 说明 |
188
+ |--------|------|
189
+ | `semantic_search_box(options)` | 渲染搜索输入框与结果容器 |
190
+ | `semantic_search_config()` | 输出自定义前端所需的配置 JSON |
191
+ | `semantic_related_posts(options)` | 渲染相关文章列表 |
192
+ | `has_semantic_related()` | 判断当前文章是否有相关文章 |
193
+ | `get_semantic_related()` | 获取原始相关文章数组 |
194
+
195
+ ## 许可
196
+
197
+ MIT
198
+
199
+ ## 友好请求
200
+
201
+ 友好请求(非强制):如果你在网站中使用了本插件,欢迎在你的网站上添加指向 https://semanticsearch.ai/ 的链接。
@@ -0,0 +1,175 @@
1
+ /**
2
+ * SemanticSearch Frontend Client
3
+ * Lightweight browser-side search client
4
+ */
5
+ (function(window) {
6
+ 'use strict';
7
+
8
+ var config = window.SEMANTIC_SEARCH_CONFIG || {};
9
+
10
+ var SemanticSearch = {
11
+ endpoint: config.endpoint,
12
+ readerKey: config.readerKey,
13
+
14
+ /**
15
+ * Configure the client
16
+ */
17
+ configure: function(options) {
18
+ this.endpoint = options.endpoint || this.endpoint;
19
+ this.readerKey = options.readerKey || this.readerKey;
20
+ },
21
+
22
+ /**
23
+ * Search documents
24
+ */
25
+ search: function(query, options) {
26
+ var self = this;
27
+ options = options || {};
28
+
29
+ return new Promise(function(resolve, reject) {
30
+ if (!self.endpoint) {
31
+ reject(new Error('SemanticSearch endpoint not configured'));
32
+ return;
33
+ }
34
+
35
+ var xhr = new XMLHttpRequest();
36
+ xhr.open('POST', self.endpoint + '/v1/search', true);
37
+ xhr.setRequestHeader('Content-Type', 'application/json');
38
+
39
+ if (self.readerKey) {
40
+ xhr.setRequestHeader('Authorization', 'Bearer ' + self.readerKey);
41
+ }
42
+
43
+ xhr.onreadystatechange = function() {
44
+ if (xhr.readyState === 4) {
45
+ if (xhr.status >= 200 && xhr.status < 300) {
46
+ try {
47
+ var response = JSON.parse(xhr.responseText);
48
+ resolve(response.results || response || []);
49
+ } catch (e) {
50
+ reject(new Error('Failed to parse response'));
51
+ }
52
+ } else {
53
+ reject(new Error('Search failed: ' + xhr.status));
54
+ }
55
+ }
56
+ };
57
+
58
+ xhr.onerror = function() {
59
+ reject(new Error('Network error'));
60
+ };
61
+
62
+ xhr.send(JSON.stringify({
63
+ query: query,
64
+ limit: options.limit || 10
65
+ }));
66
+ });
67
+ },
68
+
69
+ /**
70
+ * Bind search box with auto-complete
71
+ */
72
+ bindSearchBox: function(inputSelector, resultSelector, options) {
73
+ var self = this;
74
+ options = options || {};
75
+
76
+ var input = typeof inputSelector === 'string'
77
+ ? document.querySelector(inputSelector)
78
+ : inputSelector;
79
+ var resultContainer = typeof resultSelector === 'string'
80
+ ? document.querySelector(resultSelector)
81
+ : resultSelector;
82
+
83
+ if (!input || !resultContainer) {
84
+ console.warn('SemanticSearch: Input or result container not found');
85
+ return;
86
+ }
87
+
88
+ var debounceTimer;
89
+ var minLength = options.minLength || 2;
90
+ var debounceMs = options.debounce || 300;
91
+
92
+ input.addEventListener('input', function() {
93
+ var query = input.value.trim();
94
+
95
+ clearTimeout(debounceTimer);
96
+
97
+ if (query.length < minLength) {
98
+ resultContainer.innerHTML = '';
99
+ resultContainer.style.display = 'none';
100
+ return;
101
+ }
102
+
103
+ debounceTimer = setTimeout(function() {
104
+ self.search(query, { limit: options.limit || 10 })
105
+ .then(function(results) {
106
+ self._renderResults(resultContainer, results, options);
107
+ })
108
+ .catch(function(error) {
109
+ console.error('SemanticSearch error:', error);
110
+ resultContainer.innerHTML = '<div class="semantic-search-error">Search failed</div>';
111
+ resultContainer.style.display = 'block';
112
+ });
113
+ }, debounceMs);
114
+ });
115
+
116
+ // Hide results on click outside
117
+ document.addEventListener('click', function(e) {
118
+ if (!input.contains(e.target) && !resultContainer.contains(e.target)) {
119
+ resultContainer.style.display = 'none';
120
+ }
121
+ });
122
+ },
123
+
124
+ /**
125
+ * Render search results
126
+ */
127
+ _renderResults: function(container, results, options) {
128
+ if (!results || results.length === 0) {
129
+ container.innerHTML = '<div class="semantic-search-no-results">' +
130
+ (options.noResultsText || 'No results found') + '</div>';
131
+ container.style.display = 'block';
132
+ return;
133
+ }
134
+
135
+ var html = '<ul class="semantic-search-result-list">';
136
+ for (var i = 0; i < results.length; i++) {
137
+ var item = results[i];
138
+ // API returns: { document: { id, text, metadata }, score }
139
+ var meta = (item.document && item.document.metadata) || item.metadata || {};
140
+ var title = meta.title || (item.document && item.document.id) || item.title || 'Untitled';
141
+ var url = meta.url || item.url || '#';
142
+ var excerpt = meta.excerpt || item.excerpt || '';
143
+
144
+ html += '<li class="semantic-search-result-item">';
145
+ html += '<a href="' + url + '" class="semantic-search-result-link">' + title + '</a>';
146
+ if (excerpt && options.showExcerpt !== false) {
147
+ var shortExcerpt = excerpt.substring(0, 100) + (excerpt.length > 100 ? '...' : '');
148
+ html += '<p class="semantic-search-result-excerpt">' + shortExcerpt + '</p>';
149
+ }
150
+ html += '</li>';
151
+ }
152
+ html += '</ul>';
153
+
154
+ container.innerHTML = html;
155
+ container.style.display = 'block';
156
+ }
157
+ };
158
+
159
+ // Auto-init if config exists
160
+ if (config.endpoint) {
161
+ document.addEventListener('DOMContentLoaded', function() {
162
+ var searchBox = document.querySelector('.semantic-search-box');
163
+ if (searchBox) {
164
+ var input = searchBox.querySelector('.semantic-search-input');
165
+ var results = searchBox.querySelector('.semantic-search-results');
166
+ if (input && results) {
167
+ SemanticSearch.bindSearchBox(input, results);
168
+ }
169
+ }
170
+ });
171
+ }
172
+
173
+ window.SemanticSearch = SemanticSearch;
174
+
175
+ })(window);