【笔记】Hexo采用Elasticsearch进行全文搜索

前言

本文仅提供一种针对于 Hexo采用Elasticsearch进行全文搜索 的一个思路,如果需要复现,请对文内代码稍作改动,以便适配当前环境
如果你有更好的方案,欢迎在评论区留言

准备工作

部署Elasticsearch容器
部署ik分词器

数据库更新脚本

  • 通过Sqlite3创建一个数据表,用来存储所有文章的基本信息

采用Sqlite3存储文章的好处是可以在提交Git仓库时将数据库文件一并提交到项目中

  • 通过Python脚本,在每次更新文章之后执行脚本,将所有文章的信息保存到Sqlite3数据库

base_dir:指定Hexo项目根目录的路径,如果是相对路径就是相对于Python脚本的路径
post_cn_filename_list:指定所有文章所在的目录
database_file:指定为Sqlite3数据库文件的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import os
import sqlite3
import hashlib
import datetime
import base64

"""
采集AI摘要,保存到数据库
"""

# 数据表名
table_name = "posts"
# 切换到项目根目录路径
os.chdir("../../")
base_dir = os.getcwd()
# 中文文章文件名列表
post_cn_filename_list = os.listdir(f"{base_dir}/source/_posts/zh-CN/")
# 数据库文件
database_file = f"{base_dir}/source/scripts/data.db"

# 遍历文件名
for post_cn_filename in post_cn_filename_list:
if post_cn_filename == ".DS_Store":
continue
print(post_cn_filename)

# 获取文件绝对路径
file_src = f"{base_dir}/source/_posts/zh-CN/{post_cn_filename}"
print(file_src)
filename_without_suffix = post_cn_filename.replace(".md", "")

# 计算文件名的MD5
filename_md5 = hashlib.md5(post_cn_filename.encode("utf-8")).hexdigest()
print(f"filename_md5: {filename_md5}")

# 获取文件修改时间
updated_at = os.path.getmtime(file_src)
updated_at = datetime.datetime.fromtimestamp(int(updated_at)).isoformat()
print(f"updated_at: {updated_at}")

# 读取文件
with open(file_src) as f:
# 读取整篇文章
content_all = f.read()
content = content_all[content_all.find("## 前言"):]
content = base64.b64encode(content.encode('utf-8'))
content = str(content,'utf-8')
# print(f"content: {content}")
with open(file_src) as f:
# 读取所有行
line_list = f.readlines()
for line in line_list:
if line.startswith("title: "):
title = line.replace("title: ", "").strip()
title = base64.b64encode(title.encode('utf-8'))
title = str(title,'utf-8')
print(f"title: {title}")
elif line.startswith("sticky: "):
sticky = line.replace("sticky: ", "").strip()
print(f"sticky: {sticky}")
elif line.startswith("lang: "):
lang = line.replace("lang: ", "").strip()
print(f"lang: {lang}")
elif line.startswith("date: "):
date = line.replace("date: ", "").strip()
created_date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
created_at = created_date.isoformat()
print(f"created_at: {created_at}")
break

month = "%02d" % created_date.month
day = "%02d" % created_date.day
path = f"/zh-CN/{created_date.year}/{month}/{day}/{filename_without_suffix}"
path = base64.b64encode(path.encode('utf-8'))
path = str(path,'utf-8')
print(f"path: {path}")

# 连接数据库
conn = sqlite3.connect(database_file)
cur = conn.cursor()
# 写入数据库
print(f"REPLACE INTO {table_name} VALUES('{filename_md5}', '{title}', '{lang}', {sticky}, '{path}', '{content}', '{created_at}', '{updated_at}')")
cur.execute(f"REPLACE INTO {table_name} VALUES('{filename_md5}', '{title}', '{lang}', {sticky}, '{path}', '{content}', '{created_at}', '{updated_at}')")
conn.commit()
# 关闭数据库连接
cur.close()
conn.close()

# 连接数据库
conn = sqlite3.connect(database_file)
cur = conn.cursor()
# 查询数据库中所有的数据
cur.execute(f"SELECT filename_md5 FROM {table_name}")
result_list = cur.fetchall()
# 关闭数据库连接
cur.close()
conn.close()
# 获取数据库中的所有文件名MD5列表
filename_md5_in_database_list = []
for result in result_list:
filename_md5_in_database_list.append(result[0])
# print("数据库中的文件名MD5值", filename_md5_in_database_list)

# 获取磁盘中的所有文件名列表
filename_in_disk_list = post_cn_filename_list
# print("磁盘中的文件名MD5值", filename_in_disk_list)

# 获取磁盘中的所有文件名MD5列表
filename_md5_in_disk_list = []
for filename_in_disk in filename_in_disk_list:
# 计算文件名MD5值
filename_md5_in_disk = hashlib.md5(filename_in_disk.encode("utf-8")).hexdigest()
filename_md5_in_disk_list.append(filename_md5_in_disk)


"""
清理数据库中多余的记录
"""

# 遍历数据库中所有文件的MD5值列表,将所有数据库中存在但是磁盘中不存在数据删除
for filename_md5_in_database in filename_md5_in_database_list:
if filename_md5_in_database not in filename_md5_in_disk_list:
print("数据库中多余文件名MD5值", filename_md5_in_database)
# 连接数据库
conn = sqlite3.connect(database_file)
cur = conn.cursor()
# 删除数据
cur.execute(f"DELETE FROM {table_name} WHERE filename_md5='{filename_md5_in_database}'")
# 提交SQL
conn.commit()
# 关闭数据库连接
cur.close()
conn.close()
print("删除了数据库中的数据:", filename_md5_in_database)

后端

  • 虽然Elasticsearch可以通过Restful请求直接对数据库进行操作,但是不安全,所以通过一个后端程序进行维护

  • 创建一个配置文件用于维护不同环境下(开发环境和生产环境)的配置

app.port:Gin服务器的访问端口
es.ip:ES服务器的IP地址
es.port:ES服务器的端口号
db:通过上一步骤进行文章数据存储的数据库文件

config.conf
1
2
3
4
5
app.port=8080
es.ip=127.0.0.1
es.port=9200
es.db=posts
db=/root/hexo/data.db

对外只暴露全文搜索接口
对内暴露刷新所有文章数据的接口,如果对外暴露刷新所有文章数据的接口,可能会被DOS攻击
我采用了Nginx来允许跨域请求,如果你遇到了跨域问题,可以解除允许跨域请求中间件的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package main

import (
"encoding/base64"
"github.com/olivere/elastic/v7"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
)
import "github.com/gin-gonic/gin"

type Post struct {
FilenameMD5 string `json:"filename_md5" gorm:"primaryKey;column:filename_md5"`
TitleBase64 string `json:"title_base64" gorm:"column:title_base64" `
Title string `json:"title" gorm:"-" `
ContentBase64 string `json:"content_base64" gorm:"column:content_base64" `
Content string `json:"content" gorm:"-" `
PathBase64 string `json:"path_base64" gorm:"column:path_base64" `
Path string `json:"path" gorm:"-" `
Lang string `json:"lang" gorm:"column:lang" `
Sticky string `json:"sticky" gorm:"column:sticky" `
CreatedAt string `json:"created_at" gorm:"column:created_at" `
UpdatedAt string `json:"updated_at" gorm:"column:updated_at" `
}

func getMapping() string {
return `
{
"mappings": {
"properties": {
"title_base64": {"type": "keyword"},
"title": {"type": "text","analyzer": "standard"},
"content_base64": {"type": "keyword"},
"content": {"type": "text","analyzer": "standard"},
"path_base64": {"type": "keyword"},
"path": {"type": "keyword"},
"lang": {"type": "keyword"},
"sticky": {"type": "integer"},
"created_at": {"type": "date"},
"updated_at": {"type": "date"}
}
}
}
`
}

// 读取配置文件
var configMap map[string]string = initConfig()

func initConfig() (result map[string]string) {
// 初始化配置文件路径
var configFileSrc = "./config.conf"
// 根据启动参数修改配置文件路径
if len(os.Args) != 1 {
// 遍历所有参数
for _, arg := range os.Args {
// DEBUG: 当前遍历的参数
log.Println("当前遍历的参数:", arg)
if strings.HasPrefix(arg, "--config=") {
var argList = strings.Split(arg, "=")
if len(argList) == 1 {
continue
}
log.Println("成功捕获到自定义配置:", argList[1])
configFileSrc = argList[1]
break
}
}
}
log.Println("获取配置文件路径 成功:", configFileSrc)
// 定义配置Map
result = make(map[string]string)
// 读取配置文件
allConfig, _ := ioutil.ReadFile(configFileSrc)
log.Println("获取配置文件内容 成功:", string(allConfig))
// 分割字符串为每一行
var allConfigLineList = strings.Split(string(allConfig), "\n")
// 遍历每一行
for _, line := range allConfigLineList {
// 字符串分割得到Key-Value键值对
var keyAndValue = strings.Split(line, "=")
if len(keyAndValue) != 2 {
continue
}
// 截取Key键
var key = keyAndValue[0]
// 截取Value值
var value = keyAndValue[1]
// 加入到配置Map
result[key] = value
}
log.Println("读取配置文件 完成:", result)
return result
}

// 建立Sqlite数据库连接
var db *gorm.DB = initDB()

func initDB() (result *gorm.DB) {
log.Println("获取Sqlite数据库文件路径:", configMap["db"])
// 创建Gorm对象连接Sqlite3
if res, err := gorm.Open(sqlite.Open(configMap["db"]), &gorm.Config{}); err != nil {
log.Panicln("建立Sqlite数据库连接 失败:", err.Error())
} else {
log.Println("建立Sqlite数据库连接 成功:", res)
result = res
}
return result
}

// 建立ES数据库连接
var es *elastic.Client = initES()

func initES() (result *elastic.Client) {
var url string = "http://" + configMap["es.ip"] + ":" + configMap["es.port"]
log.Println("es url:", url)
if res, err := elastic.NewClient(
elastic.SetSniff(false),
elastic.SetURL(url),
); err != nil {
log.Panicln("建立ES数据库连接 失败:", err.Error())
} else {
log.Println("建立ES数据库连接 成功:", res)
result = res
}
return result
}

func main() {

// 创建Gin对象
engine := gin.Default()

// 允许跨域请求中间件
// engine.Use(func(context *gin.Context) {
// if context.Request.Header.Get("Origin") != "" {
// context.Header("Access-Control-Allow-Origin", "*")
// context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// context.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// context.Header("Access-Control-Allow-Credentials", "true")
// }
// if context.Request.Method == "OPTIONS" {
// context.AbortWithStatus(http.StatusOK)
// }
// context.Next()
// })

// 刷新ES数据库
engine.Handle("GET", "/refresh", func(context *gin.Context) {
// 如果不是本机,则禁止访问
if context.ClientIP() != "127.0.0.1" {
// 返回响应数据
context.JSON(http.StatusOK, "Access denied")
return
}

// 获取Sqlite数据库内的所有数据
var postList []Post
if err := db.Find(&postList).Error; err != nil {
log.Println("获取Sqlite数据库内的所有数据 失败:", err)
} else {
// log.Println("获取Sqlite数据库内的所有数据 完成:", postList)
}

// 查询ES的索引是否存在
var indexExist bool
if res, err := es.IndexExists(configMap["es.db"]).Do(context); err != nil {
log.Println("查询ES的索引是否存在 失败:", err)
} else {
log.Println("查询ES的索引是否存在 完成:", res)
indexExist = res
}

if indexExist {
// 删除ES的索引
if res, err := es.DeleteIndex(configMap["es.db"]).Do(context); err != nil {
log.Println("删除ES的索引 失败:", err)
} else {
log.Println("删除ES的索引 完成:", res)
}
// 创建ES的索引
if res, err := es.CreateIndex(configMap["es.db"]).BodyString(getMapping()).Do(context); err != nil {
log.Println("创建ES的索引 失败:", err)
} else {
log.Println("创建ES的索引 完成:", res)
}
}

// 遍历所有post数据
for _, post := range postList {

if title, err := base64.StdEncoding.DecodeString(post.TitleBase64); err != nil {
log.Println("Base64解码(TitleBase64) 失败:", err)
} else {
log.Println("Base64解码(TitleBase64) 成功:", string(title))
post.Title = string(title)
}
if content, err := base64.StdEncoding.DecodeString(post.ContentBase64); err != nil {
log.Println("Base64解码(ContentBase64) 失败:", err)
} else {
log.Println("Base64解码(ContentBase64) 成功:", string(content))
post.Content = string(content)
}
if path, err := base64.StdEncoding.DecodeString(post.PathBase64); err != nil {
log.Println("Base64解码(PathBase64) 失败:", err)
} else {
log.Println("Base64解码(PathBase64) 成功:", string(path))
post.Path = string(path)
}

// 新增单条文档
if res, err := es.Index().Index(configMap["es.db"]).Id(post.FilenameMD5).BodyJson(post).Do(context); err != nil {
log.Println("新增单条文档("+post.FilenameMD5+") 失败:", err)
} else {
log.Println("新增单条文档("+post.FilenameMD5+") 成功:", res)
}

// DEBUG: 只测试数组中的一条数据
// break
}

// 返回响应数据
context.JSON(http.StatusOK, "Ok")
})

// 查询全部
engine.Handle("GET", "/search", func(context *gin.Context) {
// 从ES索引中查询所有文档
var searchHitList []*elastic.SearchHit
if res, err := es.Search().Index(configMap["es.db"]).Do(context); err != nil {
log.Println("从ES索引中查询所有文档 失败: err:", err)
} else {
searchHitList = res.Hits.Hits
if len(searchHitList) == 0 {
log.Println("从ES索引中查询所有文档 完成: res.Hits.Hits 为空数组")
} else {
log.Println("从ES索引中查询所有文档 完成: searchHitList:", searchHitList)
}
}

// 解析击中的文档数据
var resultList []string
for _, searchHit := range searchHitList {
log.Println("searchHit.Id:", searchHit.Id)
if res, err := searchHit.Source.MarshalJSON(); err != nil {
log.Println("获取击中的文档数据 失败:", err.Error())
} else {
log.Println("获取击中的文档数据 完成:", string(res))
resultList = append(resultList, string(res))
}
}

context.JSON(http.StatusOK, resultList)
})

// 搜索关键词
engine.Handle("GET", "/search/:keywords", func(context *gin.Context) {
// 获取关键词
var keywords string
if res := context.Param("keywords"); res == "" {
log.Println("获取关键词 失败: 获取请求参数失败")
// 返回响应数据
context.JSON(http.StatusOK, nil)
return
} else {
log.Println("获取关键词 完成:", res)
keywords = res
}

// 全文检索
var searchHitList []*elastic.SearchHit
// 根据关键词从ES索引中查询文档
if res, err := es.Search().Index(configMap["es.db"]).Query(elastic.NewMatchPhraseQuery("content", keywords)).Do(context); err != nil {
log.Println("根据关键词从ES索引中查询文档 失败: err:", err)
} else if res.Error != nil {
log.Println("根据关键词从ES索引中查询文档 失败: res.Error:", res.Error)
} else {
searchHitList = res.Hits.Hits
if len(searchHitList) == 0 {
log.Println("根据关键词从ES索引中查询文档 完成: res.Hits.Hits 为空数组")
} else {
log.Println("根据关键词从ES索引中查询文档 完成: searchHitList:", searchHitList)
}
}

// 解析击中的文档数据
var resultList []string
for _, searchHit := range searchHitList {
log.Println("searchHit.Id:", searchHit.Id)
if res, err := searchHit.Source.MarshalJSON(); err != nil {
log.Println("获取击中的文档数据 失败:", err.Error())
} else {
log.Println("获取击中的文档数据 完成:", string(res))
resultList = append(resultList, string(res))
}
}

context.JSON(http.StatusOK, resultList)
})

if err := engine.Run(":" + configMap["app.port"]); err != nil {
log.Panicln("Gin启动服务 失败:", err.Error())
}
}

直接启动

1
2
go build main.go
./main

通过配置文件启动

<config>:配置文件路径

1
2
go build main.go
./main --config=<config>

编写一个Hook脚本

  • 每当文章被更新,自动触发Hook刷新所有文章数据
/root/hook.shell
1
curl http://127.0.0.1:8080/refresh

Nginx反向代理

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name es-search.loli.fj.cn;

location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

proxy_pass http://localhost:8080;
}
}

前端

  • 设置双击放大镜图标的事件,加载Elasticsearch搜索引擎

http://example.com/:指定后端API的访问URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
* 通过Elasticsearch进行搜索
*/
let search = "local";

function initElasticsearch() {
// 放大镜图标
const search_icon = document.querySelector(".search-pop-overlay .search-icon");
// 搜索框
const search_input = document.querySelector(".search-pop-overlay .search-input");
// 搜索结果框
const search_output = document.querySelector(".search-pop-overlay .search-result-container");
// 初始化搜索框内的文字为Local搜索
search_input.placeholder = "Local搜索...";
// 放大镜双击事件
search_icon.addEventListener("dblclick", function () {
if (search === "local") {
// 搜索框内的文字切换为Elasticsearch搜索
search_input.placeholder = "Elasticsearch搜索...";
console.log("恭喜你发现了一个彩蛋,现在可以通过Enter来使用Elasticsearch进行全文搜索了,点击放大镜将继续使用Local搜索");
search = "elasticsearch";

// 克隆搜索框节点,以清除所有事件
const new_search_input = search_input.cloneNode(true);
search_input.parentNode.append(new_search_input);
search_input.parentNode.removeChild(search_input);

// 克隆搜索图标节点,以清除所有事件
const new_search_icon = search_icon.cloneNode(true);
search_icon.parentNode.append(new_search_icon);
search_icon.parentNode.removeChild(new_search_icon);

// 切换为Elasticsearch搜索
changeSearchToElasticsearch(new_search_icon, new_search_input, search_output);
}
});
}

function changeSearchToElasticsearch(search_icon, search_input, search_output) {

function Dec2Dig(n1) {
let s = "";
let n2 = 0;
for (let i = 0; i < 4; i++) {
n2 = Math.pow(2, 3 - i);
if (n1 >= n2) {
s += "1";
n1 = n1 - n2;
} else
s += "0";
}
return s;
}

function Dig2Dec(s) {
let retV = 0;
if (s.length === 4) {
for (let i = 0; i < 4; i++) {
retV += eval(s.charAt(i)) * Math.pow(2, 3 - i);
}
return retV;
}
return -1;
}

function Hex2Utf8(s) {
let retS = "";
let tempS = "";
let ss = "";
if (s.length === 16) {
tempS = "1110" + s.substring(0, 4);
tempS += "10" + s.substring(4, 10);
tempS += "10" + s.substring(10, 16);
let sss = "0123456789ABCDEF";
for (let i = 0; i < 3; i++) {
retS += "%";
ss = tempS.substring(i * 8, (eval(i) + 1) * 8);
retS += sss.charAt(Dig2Dec(ss.substring(0, 4)));
retS += sss.charAt(Dig2Dec(ss.substring(4, 8)));
}
return retS;
}
return "";
}

function Str2Hex(s) {
let c = "";
let n;
let ss = "0123456789ABCDEF";
let digS = "";
for (let i = 0; i < s.length; i++) {
c = s.charAt(i);
n = ss.indexOf(c);
digS += Dec2Dig(eval(n));
}

return digS;
}

function EncodeUtf8(s1) {
let s = escape(s1);
let sa = s.split("%");
let retV = "";
if (sa[0] !== "") {
retV = sa[0];
}
for (let i = 1; i < sa.length; i++) {
if (sa[i].substring(0, 1) === "u") {
retV += Hex2Utf8(Str2Hex(sa[i].substring(1, 5)));
} else retV += "%" + sa[i];
}
return retV;
}

function search_to_elasticsearch(keywords) {
console.log("正在使用Elasticsearch搜索");
// 清除结果列表
search_output.innerHTML = `
<div class="search-stats">正在使用Elasticsearch全文搜索...</div>
<hr>
<ul class="search-result-list"></ul>
`;
const result_header = document.querySelector(".search-pop-overlay .search-stats");
const result_ul = document.querySelector(".search-pop-overlay .search-result-list");

fetch(`http://example.com/search/${keywords}`).then((response) => {
return response.json();
}).then((results) => {
if (results == null) {
result_header.innerHTML = "找到 0 个搜索结果";
} else {
const result_count = results.length;
result_header.innerHTML = `找到 ${result_count} 个搜索结果`;
// 遍历结果集
for (let result of results) {
result = JSON.parse(result);
// 解析标题
const title_base64 = result["title_base64"];
let title = window.atob(title_base64);
title = decodeURIComponent(EncodeUtf8(title));
// 解析路径
const path_base64 = result["path_base64"];
let path = window.atob(path_base64);
path = decodeURIComponent(EncodeUtf8(path));
// 创建超链接节点
const a = document.createElement("a");
a.href = `${path}?highlight=${keywords}`;
a.className = "search-result-title";
a.setAttribute("data-pjax-state", "");
a.innerHTML = title;
// 创建列表项节点
const li = document.createElement("li");
// 追加超链接节点到列表项节点
li.append(a);
// 追加列表项节点到无序列表节点
result_ul.append(li);
}
}
});
}

search_icon.addEventListener("click", function () {
search_to_elasticsearch(search_input.value);
});
search_input.addEventListener("keydown", function (event) {
if (event.keyCode === 13) {
search_to_elasticsearch(search_input.value);
}
});
}

initElasticsearch();

完成

  • 现在可以先打开搜索窗口,然后双击放大镜图标以激活Elasticsearch全文搜索,输入关键词后,通过Enter键来使用Elasticsearch进行全文搜索,点击放大镜将继续使用Local搜索