内容中心-搜索设计
项目背景:
收到一个P0级产品需求
简单说下产品需求:
将系统重已有的一些独立内容模块(文章、视频、图片、海报、商品、话术.....)整合到一起。包含收藏、常用、筛选、搜索、历史搜索记录、搜索结果高亮等配套功能。
模型设计:
内容实体模型:
// SearchContent 搜索内容结构
type SearchContent struct {
ID string `json:"id"`
Type string `json:"contentType,omitempty"` // 内容类型
SubType string `json:"subContentType,omitempty"` // 内容子类型
Content []Content `json:"content,omitempty"` // 文本内容对象数组 (标题、副标题、分享标题、文本内容、摘要等需要进行搜索的文案)
CategoryId string `json:"categoryId,omitempty"` // 文件夹ID
LastSendTime int64 `json:"lastSendTime,omitempty"` // 最近一次发送时间
CreateUser string `json:"createUser,omitempty"` // 创建人ID
UpdateTime int64 `json:"updateTime,omitempty"` // 最近更新时间
CreateTime int64 `json:"createTime,omitempty"` // 创建时间
OnlineTime int64 `json:"onlineTime"` // 上架时间 - 起点
OfflineTime int64 `json:"offlineTime"` // 上架时间 - 截止
VisibleUserIDs []string `json:"visibleUserIDs"` // 可见权限-员工
VisibleDepartments []int64 `json:"visibleDepartments"` // 可见权限-部门
SendCount uint32 `json:"sendCount,omitempty"` // 总发送次数
Rank1 uint32 `json:"rank1"` // 排序字段
Rank2 uint32 `json:"rank2"` // 排序字段2
Rank3 uint32 `json:"rank3"` // 排序字段3
}
// Content 内容详情
type Content struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"` // 标题
Text string `json:"text,omitempty"` // 内容详情
Summary string `json:"summary,omitempty"` // 摘要
Type string `json:"contentType,omitempty"` // 内容类型
Tags []Tag `json:"tags,omitempty"`
}
// Tag 内容标签
type Tag struct {
ID string `json:"id,omitempty"` // 标签ID
Name string `json:"name"` // 标签名称
}
模型设计思路:
之所以会设计成用content结构嵌套,一是由于话术的特性(一个话术可以包含N个子话术,而子话术可以是除话术外的所有类型内容实体),二是层级的加入使得模型整体结构不至于那么臃肿。
数据同步
那么这么多内容,如何同步到es中呢?要知道这些内容都存在于不同的表甚至存在于不同的数据库中,甚至可能来自于不同的三方平台。
需要考虑的主要问题
1、 数据最终一致性
2、 实时性
3、 数据竞争
结合以上3点主要问题,我们采用了以下设计方案
为避免数据竞争,这里采用了pulsar/kafka的shared模式,key使用binlog的主键ID,这样保证了同一条数据只会被传输到同一个patition中,实现了数据的有序性,当然这里也可以用有序队列进行替换。
而到了server这里层, 不信任binlog的值,而是采用主键id点查的方式查询最新数据进行更新,也就规避了数据最终一致性的问题。当然这样做也是有代价的,数据库的io也会有一定增长。
采用了这一套同步机制,实时性根据量级会有一定的牺牲。
设计模式的选择
策略模式
上述说到,内容中心es中涵盖了所有类别的内容实体,那么这个时候运用策略模式,根据不同的SearchContent.Type走各个实体自己的逻辑无疑是很好的选择之一。
注册策略
var (
searchCenterStrategy = make(map[entity.ContentType]func(ctx context.Context) IntelligentSearchTranslator)
)
// Register 注册策略模式
func Register(t []entity.ContentType, f func(ctx context.Context) IntelligentSearchTranslator) {
for _, tp := range t {
searchCenterStrategy[tp] = f
}
}
好的,策略已注入,IntelligentSearchTranslator这个下面会提到,不急不急
简易工厂模式
interface设计
// IntelligentSearchTranslator 数据转换器
type IntelligentSearchTranslator interface {
// 转换为更新插入的数据结构
TranslateForUpdate(contentId, contentType string) (data entity.SearchContent, exist bool, err error)
// 查询详情并转换为统一结构
SearchAndTranslate(contentIds []string, dataList []entity.SearchContent) ([]vo.SearchResponse, error)
}
这里,IntelligentSearchTranslator作为转换器,只实现2种功能
1、 TranslateForUpdate将源数据转换为SearchContent格式,也就是es中的数据格式
2、 SearchAndTranslate在查询时使用,反查数据详情
有同学可能在想,SearchAndTranslate这个方法是不是有点冗余了,把数据全部存到es中,直接反查不就好了吗?
针对这个问题,需要看业务场景,像一些简单的场景下,只需要反显一些简单的text,那么也就无需这个操作。但如果涉及到补充的数据过多,有的甚至有关联的数据,那么关联的数据是不是也要监听并同步到es中呢?这样设计的话反而更复杂了。 例如:商品item需要展示店铺详情,店铺信息本身不参与搜索,但是要反显到列表中,这时候SearchAndTranslate反查一下信息就好,但如果存到es中,还要多监听店铺信息的表信息,设计上会很麻烦。再例如:若果反查的信息需要调用三方接口,那是否得要求三方接口提供回调功能,否则无法监听数据变更。
分页设计
es的深度分页是避不开的问题,那么整合下es的分页方式,有如下比较。结合实际情况选取适合的分页方式。
| 分页方式 | 优势 | 劣势 |
|---|---|---|
| from + size | 实时性强 | 1、 可能出现数据index变更2、 无法深度分页 |
| scroll | 深度分页不受数据增、删影响 | 1、 查询期间无法感知数据的增和删2、 内存消耗大3、 无法连续分页(跳页 |
| scroll scan | scroll升级版,性能有提升 | 1、2、3与scroll相同4、不支持排序 |
| search after | 性能最佳,无深度分页问题 | 无法连续分页(跳页)但最低需求7.1.0版本 |
小结
内容中心的搜索设计大体上就如上所述。本文旨在分享下使用es做搜索引擎以及数据同步的设计,如果各位有做过类似功能或者有更好建议欢迎在评论区讨论。下期分享下es的一些语法,以及如何用这些语法实现复杂的业务功能。