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 +201 -0
- package/README.zh-CN.md +201 -0
- package/assets/semantic-search.js +175 -0
- package/index.js +201 -0
- package/lib/client.js +142 -0
- package/lib/helpers.js +96 -0
- package/lib/related.js +283 -0
- package/lib/state.js +91 -0
- package/lib/sync.js +159 -0
- package/package.json +37 -0
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.
|
package/README.zh-CN.md
ADDED
|
@@ -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);
|