查看: 750|回复: 0

优化了破网站的搜索功能

[复制链接]

144

主题

0

回帖

444

积分

中级会员

积分
444
 楼主| 发表于 2021-8-2 18:02:18 | 显示全部楼层 |阅读模式
使用 ES + 云开发实战优化网站搜索
+ I, [4 [$ O# c0 s/ D$ @
大家好,我是鱼皮,今天搞一场技术实战,需求分析 => 技术选型 => 设计实现,从 0 到 1,带大家优化网站搜索的灵活性。
5 z) P4 S. T) b
ES + 云开发搜索优化实战' e  ^$ \4 z" Q# X. O

+ t8 {, }  z- }4 g$ P5 H3 I
本文大纲:
; ], ^6 B) _' p0 k  C$ Z+ c5 w

: Y7 A) {% d4 o/ A: Z4 L7 g

优化了破网站的搜索功能

优化了破网站的搜索功能

$ o# s5 q. C' l/ c* v( f  h
鱼皮 - 网站搜索优化

- l5 K' {0 B6 n; _+ l  O% W9 b背景
+ E6 Y  ?2 B2 u; F9 m
: g9 O7 m3 x1 P
我开发的 编程导航网站 已经上线 6 个月了,但是从上线之初,网站一直存在一个很严重的问题,就是搜索功能并不好用。
! Z/ Z) U$ Y+ k9 K; G- O
此前,为了追求快速上线,搜索功能就简单地使用了数据库模糊查询(包含)来实现,开发是方便了,但这种方式很不灵活。

9 m4 K- h+ A2 W& D/ e( \& e3 ?: ]
举个例子,网站上有个资源叫 “Java 设计模式”,而用户搜索 “Java设计模式” 就啥都搜不出来,原因是资源名中包含了空格,而用户搜索时输入的关键词并不包含空格。
5 \7 v* d1 ?/ u& r9 g- O
空格只是一种特例,类似的情况还有很多,比如网站上有个资源叫 “Java 并发编程实战”,但用户搜索 “Java 实战” 时,明明前者包含 “Java” 和 “实战” 这两个词,但却是什么都搜不出来的。

, b: D- K9 y8 D0 E8 I4 e: D" Z% M
要知道,搜索功能对于一个信息聚合类站点是至关重要的,直接影响用户的体验。在你的网站上搜不到资源,谁还会用?
- K3 y1 H# Y5 t8 R) M
所以我也收到了一些小伙伴的礼貌建议,比如这位秃头 Tom:

- ~& \5 G% A& T
/ k& q7 R$ ~5 f* N/ y# [# L

优化了破网站的搜索功能

优化了破网站的搜索功能

& q1 Y- r) Y2 [5 j+ G, o
之前没有优化搜索,主要是两个原因:穷 + 怕麻烦。但随着网站用户量的增大,是时候填坑了!
: E& e# B+ F$ P( l$ c' S: Q, f+ @
技术选型
- y6 ^0 ?; ?& q2 x8 V% _9 d" D
! l' s; p# p: I
想要提高网站搜索灵活性,可以使用 全文搜索 技术,在前端和后端都可以实现。
1 J: r" p- [% j3 \  m
前端全文搜索( `7 _) S' I+ A9 H2 n8 D! S0 e4 E) l# t

- m4 E# K7 I* M5 w8 F
有时,我们要检索的数据是有限的,且所有数据都是 存储在客户端 的。

' k2 b, F7 p# y# M- f9 V5 E; T( g  R% |
比如个人博客网站,我们通常会把每篇文章作为一个文件存放在某目录下,而不是存在后台数据库中,这种情况下,不需要再从服务器上去请求动态数据,那么可以直接在前端搜索数据。
+ s, v) w7 L- e/ z" S/ |
有一些现成的搜索库,比如 Lunr.js(GitHub 7k+ star),先添加要检索的内容:

4 V. W2 J* F+ a4 E( E0 C" C: Evar idx = lunr(function () {  this.field('title')  this.field('body')  // 内容  this.add({    "title": "yupi",    "body": "wx搜程序员鱼皮,阅读我的原创文章",    "id": "1"  })})
然后搜索就可以了:
, H1 J# F/ E$ b. J0 p0 }
idx.search("鱼皮")
纯前端全文搜索的好处是无需后端、简单方便,可以节省服务器的压力;无需连网,也没有额外的网络开销,检索更快速。
$ i: z# H( N. x) L, J
后端全文搜索
4 ~* ]* A4 v/ `* ]1 V1 D8 O4 q6 X( d; u' F# d" z0 F
区别于前端,后端全文搜索在服务器上完成,从远程数据库中搜索符合要求的数据,再直接返回给前端。

5 P" c: r- }) ~6 d
目前主流的后端全文搜索技术是 Elasticsearch,一个分布式、RESTful 风格的搜索和数据分析引擎。

% t, s7 r! b. T0 a  m& A) n& ~  |9 d" w; c( c8 B* L$ d0 r$ s

优化了破网站的搜索功能

优化了破网站的搜索功能

$ W: M8 U% B" I$ q! {
它的功能强大且灵活,但是需要自己搭建、定义数据、管理词典、上传和维护数据等,可操作性很强,需要一些水平,新手和大佬设计出的 ES 搜索系统那是天差地别。
* Z- h8 `% l3 G; ?# @
所以,对于不熟悉 Elasticsearch 的同学,也可以直接使用现成的全文检索服务。比如 Algolia,直接通过它提供的 API 上传需要检索的数据,再用它提供的 API 检索就行了。它提供了一定的免费空间,对于小型网站和学习使用完全足够了。

; Y0 M  n& L1 w, B: `9 z; B7 N& y& ]8 h) B

优化了破网站的搜索功能

优化了破网站的搜索功能

7 g. ]4 |5 u8 [4 _) K
Algolia 检索服务

5 S4 i, Y9 @4 d  \' T' \4 i选择
  z2 L9 M3 Y; {8 }
/ S$ o; D- v( f, ]5 C& s
那么我的编程导航网站选择哪种实现方式呢?

. s4 d9 [; H, A4 y
首先,该网站的资源数是不固定的、无规律动态更新的,因此不适合前端全文检索。
- P2 _! P: l9 g1 q. v
其次,考虑到日后网站的数据量会比较大,而且可能要根据用户的搜索动态地去优化检索系统(比如自定义编程词典),因此考虑使用 Elasticsearch 技术 自行搭建搜索引擎,而不用现成的全文检索服务,这样今后自己想怎么定制系统都可以。此外,不用向其他平台发送网站数据,能保证数据的安全。

5 C& [0 o# Q9 T7 l% s9 b8 V1 _' Y# D

优化了破网站的搜索功能

优化了破网站的搜索功能

3 G$ i7 G2 ?  {6 H( `8 c$ T% LES 安装
# [" y# {- L8 ?8 b& K
- ]4 ?* ^/ |" f% @. c1 w
确定使用 Elasticsearch 后,要先搭建环境。
4 q5 v8 i3 B9 ?2 v
可以自己购买服务器,再按照官方文档一步步手动安装。对于有一定规模的个人网站来说,虽然搭建过程不难,但后期的维护成本却是巨大的,比如性能分析、监控、告警、安全等等,都需要自己来配置。尤其是后期网站数据量更大了,还要考虑搭建集群、水平扩容等等。

8 g1 i5 ~# [9 ?4 T
因此,我选择直接使用云服务商提供的 Elasticsearch 服务,这里选择腾讯云,自动为你搭建了现成的 ES 集群服务,还提供了可视化架构管理、集群监控、日志、高级插件、智能巡检等功能。

) Y5 ]; K- \2 Y" e+ E
! T" K9 w, p. @  k% k

优化了破网站的搜索功能

优化了破网站的搜索功能

2 {2 }0 M0 O2 W2 g8 W* p
云 ES 集群架构图
4 o* {$ z. ]0 V
虽然 ES 服务的价格贵,但节省下大量时间成本,对我来说是值得的。
* |$ }9 T3 R7 }( c/ e
还有个很方便的定制化搜索服务 Elastic App Search,大家感兴趣可以试试。

; S% _9 E; T, v( ?& k
ES 公共服务
, b' u% z( [& ?5 m* l( w7 X5 T( c, Q6 _' q/ o* z  I+ ^- e
我们的目标是优化网站资源的搜索功能,但接下来要做的不是直接编写具体的业务逻辑,而是先开发一个 公共的 ES 服务

9 E* m/ i: W* |) v* j" w( W
其实对 ES 的操作比较简单,可以先简单地把它理解为一个数据库,那么公共的 ES 服务应具有基本的增删改查功能,供其他函数调用。

3 w5 z% t2 e( {1 x. p实现
" H7 }& ?" M  P, e' d" o0 `* _. `% P
由于编程导航的后端使用的是腾讯云开发技术,用 Node.js 来编写服务,所以选用官方推荐的 @elastic/elasticsearch 库来操作 ES。
, {5 G7 j7 P* y1 G+ }$ @
没用过云开发也没事,可以先把它理解为一个后端,欢迎阅读我之前的文章:了解云开发 。

, B& S  e8 ?$ X  S  y5 |% A% t6 i
代码很简单,先是建立和 ES 的连接,此处为了保证数据安全,使用内网地址:
" r& B* X' S  J7 k, [1 Q7 d% K) s! i
const client = new Client({ // 内网地址  node: 'http://10.0.61.1:9200',  // 用户名和密码  auth: {    username: esConfig.username,    password: esConfig.password,  },});
然后是编写增删改查。这里做一步 抽象,通过 switch 等分支语句,根据请求参数来区分操作、要操作的数据等,这样就不用把每个操作都独立写成一个接口了。

$ D( h6 j3 ~! B6 H' W// 接受请求参数const { op, index, id, params } = event;// 根据操作执行增删改查switch (op) {  case 'add':    return doAdd(index, id, params);  case 'delete':    return doDelete(index, id);  case 'search':    return doSearch(index, params);  case 'update':    return doUpdate(index, id, params);}
在云开发中,假如某个函数太久没被调用,就会释放资源。下次请求时,会进行冷启动,重新创建资源,导致接口返回较慢。因此,把多个操作封装到同一个函数中,也可以减少冷启动的几率。
1 M( ~* V1 ?% T; O
具体的增删改查代码就不赘述了,对着 ES Node 的官方文档看一遍就行了,后面会把代码开源到编程导航仓库中(https://github.com/liyupi/code-nav)。
$ u" {# t; W) P, y. \: S: q* Q
本地调试
: @7 ~, l$ e4 P5 Y, a3 v& Y- i
2 t( A6 O* o# F& F6 l
编写好代码后,可以用云开发自带的 tcb 命令行工具在本地执行该函数。
1 |5 i) e* H4 E- \
记得先把 ES 的连接地址改成公网,然后输入一行命令就行了。比如我们要向 ES 插入一条数据,传入要执行的函数名、请求参数、代码路径:

5 Q, P7 ?! C5 W# b( k2 qtcb fn run   --name <functionName>  --params "{\"op\": \"add\"}"  --path <functionPath>
执行成功后,就能在 ES 中看到新插入的数据了(通过 Kibana 面板或 curl 查看):
/ ~. f/ @* `: |1 E
$ m5 v9 c) i$ D

优化了破网站的搜索功能

优化了破网站的搜索功能
) V& a" s( H9 W$ {. g9 Y
远程测试
5 T) g' K- Y$ a2 a
( ?  A/ \+ n1 t' J1 A" `
本地测试好公共服务代码后,把 ES 连接地址改成内网 IP,然后发布到云端。

3 i* M% i5 L4 i+ a1 U4 k9 p
接下来试着编写一个其他的函数来访问公共 ES 服务,比如插入资源到 ES,通过 callFunction 请求:

4 [: T6 @$ h4 ]# p" t// 添加资源到 ESfunction addData() {  // 请求公共服务  app.callFunction({    name: 'esService',    data: {      op: 'add',      index: 'resource',      id,      params: data,    }  });}
但是,数据并没有被成功插入,而是返回了接口超时,Why?
5 f2 `: S% n' u8 c0 I' O
内网配置
5 ^3 J5 m. T1 {
. f0 \0 E, Z. E1 j" k  O" F
通过日志得知是 ES 连接不上,会不会是因为发布上线的 ES 公共服务所在的机器和 ES 不在同一个内网呢?
  m7 w7 R" I; Y% }* g, _" F
所以需要在云开发控制台更改 ES 公共服务的私有网络配置,选择和购买 ES 时同样的子网就行了:
1 g' o8 s: L% d# k/ f5 Z) A
  s2 ]# g6 s6 m

优化了破网站的搜索功能

优化了破网站的搜索功能

# x1 |2 v# P. j. D' M# d
配置 ES 云函数私有网络
1 X; x; W2 v5 p" C  k8 V
修改之后,再次远程请求 ES 公共服务,数据就插入成功了~

0 P2 p- a# X$ s" `数据索引
5 t. w. `. |$ E9 O! m# O7 _4 v$ b& `# Q* C2 }' K' b
开发好 ES 公共服务后,就可以编写具体的业务逻辑了。

( G8 E6 J- Q7 [+ r+ t; O
首先要在 ES 中建立一个索引(类似数据库的表),来约定数据的类型、分词等信息,而不是允许随意插入数据。

) O+ R0 A5 P2 Q( P+ I; `3 k
比如为了更灵活搜索,资源名应该指定为 "text" 类型,以开启分词,并指定 ik 中文分词器:

5 C; ]1 g; f- ~"name": {  "type": "text",  "analyzer": "ik_max_word",  "search_analyzer": "ik_smart",  "fields": {    "keyword": {      "type": "keyword",      "ignore_above": 256    }  }}
而点赞数应设置为 "long" 类型,只允许传入数字:

. e) |0 ?' Z4 B+ r$ C"likeNum": {  "type": "long"}
最好还要为索引指定一个别名,便于后续修改字段时重建索引:

6 i% S) T; [  ?: G# x"aliases" : {   "resource": {}}
编写好建立索引的 json 配置后,通过 curl 或 Kibana 去调用 ES 新建索引接口就行了。

, R( r6 j/ u# G( f数据同步  ^1 I$ i0 m: T, O. o- }3 z2 ]( r
7 F1 Q' t  S0 X2 [9 |2 E" M. h$ j" `' ]7 j
之前,编程导航网站的资源数据都是存在数据库中的,用户从数据库中查询。而现在要改为从 ES 中查询,ES 空空如也可不行,得想办法把数据库中的资源数据同步到 ES 中。

1 p! q- p& a- c3 D! r
这里有几种同步策略。

8 R8 ^2 d6 a7 u7 T7 f+ z双写* D6 R. M) j1 B& l( R* p/ J

) X+ f6 j! H2 h2 R8 c
以前,用户推荐的资源只会插入到数据库,双写是指在资源插入数据库的时候,同时插入到 ES 就好了。

9 N. y' z, [" r7 N8 p1 [* X
听上去挺简单的,但这种方式存在一些问题:

6 i% w5 u# x. y' Y$ Z- Q& b+ [- k6 @
  • 会改动以前的代码,每个写数据库的地方都要补充写入 ES。
  • 会存在一边儿写入失败、另一边儿成功的情况,导致数据库和 ES 的数据不一致。
    ; R8 e+ J3 V; t+ u4 W

3 H4 M% G) |3 Q2 L4 d1 g  P

优化了破网站的搜索功能

优化了破网站的搜索功能
' U9 }8 W- w2 D  W$ w( `
那有没有对现有代码 侵入更小 的方法呢?

* U( D" @4 J8 G% l. o定时同步
" i0 O3 O$ c# `. N3 p9 ^! C
! K& `; U6 Y: ~" V/ F* `0 ]8 A, c
如果对数据实时性的要求不高,可以选择定时同步,每隔一段时间将最新插入或修改的数据从数据库复制到 ES 上。

: }8 Q* Z* x$ L9 K' D/ _7 A
实现方式有很多种,比如用 Logstash 数据传输管道,或者自己编写定时任务程序,这样就完全不用改现有的代码。
, ?% u7 I2 M3 \+ k$ g1 ^, A. K
实时同步
( \( U  |) a1 E) u5 ?3 G0 K0 d" [- c7 [) m0 ^
如果对数据实时性要求很高,刚刚插入数据库的数据就要能立刻就能被搜索到,那么就要实时同步。除了双写外,还可以监听数据库的 binlog,在数据库发生任何变更时,我们都能感知到。
7 y3 Z5 Y  g% `7 M0 g  o) J
阿里有个开源项目叫 Canal ,能够实时监听 MySQL 数据库,并推送通知给下游,感兴趣的朋友可以看看。
' U" V- m# \( ~/ S
  O0 h3 O' C/ }! J7 [* P! Z% p% L1 d6 H

优化了破网站的搜索功能

优化了破网站的搜索功能

* O) t: w* q! G6 \7 |
Canal 项目
( X) q$ e8 D. j+ L
实现
, {0 y: F  F/ |5 u, N& X4 T# F
! O" c) M  L+ q6 W; L' U
由于编程资源的搜索对实时性要求不高,所以定时同步就 ok。
# M1 ]8 ?# K, s. i( E" [4 Z
云开发默认提供了定时函数功能,我就直接写一个云函数,每 1 分钟执行一次,每次读取数据库中近 5 分钟内发生了变更的数据,以防止上次执行失败的情况。此外,还要配置超时时间,防止函数执行时间过长导致的执行失败。

% t8 f+ v! G6 s
在云开发 - 云函数控制台就能可视化配置了,需要为定时任务指定一个 crontab 表达式:

3 a! o: c1 j" O) L
# e0 T) v+ Y( y

优化了破网站的搜索功能

优化了破网站的搜索功能
0 u# s1 @" ]* }3 Z  W. L
配置云函数定时和超时

+ F; v+ Z5 m5 o- K  k8 I) ~" K
开启定时同步后,不要忘了再编写并执行一个 首次 同步函数,用于将历史的全量数据同步到 ES。
, l/ ~5 v$ i0 \1 C2 ^: C7 F
数据检索$ U: @$ l$ \+ b; P1 S/ B$ V5 v1 [
2 C3 v) U5 k9 u9 d* b" C- x
现在 ES 上已经有数据了,只剩最后一步,就是怎么把数据搜出来呢?
  w! l6 Q' v5 b7 X. x, X
首先我们要学习 ES 的搜索 DSL(语法),包括如何取列、搜索、过滤、分页、排序等,对新手来讲,还是有点麻烦的,尤其是查询条件中布尔表达式的组合,稍微不注意就查不出数据。所以建议大家先在 Kibana 提供的调试工具中编写查询语法:

0 d8 w: X- u- ~. ?+ v1 P+ Y! D# [- W& }/ u0 a

优化了破网站的搜索功能

优化了破网站的搜索功能
/ i( Y% d, H' {# l5 y
Kibana 调试
0 D6 P( [. [1 F3 N+ j
查出预期的数据后,再编写后端的搜索函数,接受的请求参数最好和原接口保持一致,减少改动。

' J7 T& S& f- y3 A
可以根据前端传来的请求动态拼接查询语法,比如要按照资源名搜索:

) I* e/ n0 ^% ]: K% F) Y* n// 传了资源名if (name) {  // 拼接查询语句  query.bool.should = [    {      match: {        name      }    }  ];}
由此,整个网站的搜索优化完毕。

/ k! n; d) }6 V/ t  C" ~9 I* e
再去试一下效果,现在哪怕我输入一些多 “鱼” 的词,也能搜到了!
0 u- F- x$ w, f  |7 z; k
* C; h/ V4 ~6 v+ j# U

优化了破网站的搜索功能

优化了破网站的搜索功能
; z5 T$ S7 x% N
ES 是怎么实现灵活搜索的呢?欢迎阅读 这篇文章 。
5 o' f4 g& r1 H% R3 M7 v* }* X/ O
新 ES 搜索接口的发布并不意味着老的数据库查询接口淘汰,可以同时保留。按名称搜索资源时用新接口,更灵活;而根据审核状态、搜索某用户发布过的资源时,可以用老接口,从数据库查。从而分摊负载,职责分离,让对的技术做对的事情!
/ v6 f/ _8 {' m$ b  v3 `
<hr>
以上就是本期分享,有帮助的话点个赞吧 ❤️
& D: F2 B2 o6 l8 A& w" y) Y/ n
我是鱼皮,最后再送大家一些 帮助我拿到大厂 offer 的学习资料

4 j% i' d# _% b3 h' p7 k( |6 U
指路:https://t.1yb.co/qOJG
6 a. n4 {; i$ a
欢迎阅读 我从 0 自学进入腾讯的编程学习、实习、求职、考证、写书经历,不再迷茫!

# a$ N0 s# I: o/ S, G( M$ H. V
指路:https://t.1yb.co/w66s

2 T4 K: ?/ L( A0 h0 k7 `! X' a4 K7 {* \6 V3 t' }

优化了破网站的搜索功能

优化了破网站的搜索功能
4 k. q$ k( j1 T1 P2 V3 o& }7 N8 W
, p8 p) M. j) [  c4 y9 W0 N  G
原文链接:程序员鱼皮|关注整站优化网 学习更多SEO相关方法...
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|SEO整站优化 — 外包整站SEO优化,万维网【SEO外包】整站优化网 ( 粤ICP备19158344号 ) |

GMT+8, 2024-3-29 03:53 , Processed in 0.085397 second(s), 13 queries , Gzip On, MemCached On.

Powered by Discuz! X3.5

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表