Tag 是一个很常见的功能,这篇文章将使用 Go + MySQL + ES 实现一个 500 多行的 tag API 服务,支持 创建/搜索 标签、标签关联到实体 和 查询实体所关联的标签列表。
初始化环境
MySQL
1 | 复制代码brew install mysql |
ES
这里直接通过 docker 来启动 ES:
1 | 复制代码docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch |
启动后可以通过 curl 检查是否已经启动和获取版本信息:
1 | 复制代码curl localhost:9200 |
注意上面的部署仅用于开发环境,如果需要在生产部署通过 docker 部署,请参考官方文档: Install Elasticsearch with Docker。
设计存储结构
先在 MySQL 里面创建一个 test 数据库:
1 | 复制代码create database test; |
创建 tag_tbl 表:
1 | 复制代码CREATE TABLE `tag_tbl` ( |
tag_tbl 用于存储标签,注意这里给我们给 name 字段加上了一个唯一键,并使用 hash 作为索引方法,关于 hash 索引,可以参考官方文档:Comparison of B-Tree and Hash Indexes。
再创建 entity_tag_tbl 用于存储实体关联的 tag:
1 | 复制代码CREATE TABLE `entity_tag_tbl` ( |
设计 API
创建标签
Request:
1 | 复制代码POST /api/tag |
Response:
1 | 复制代码{ |
搜索标签
Request:
1 | 复制代码GET /api/tag/search |
Response:
1 | 复制代码{ |
关联标签到实体
Request:
1 | 复制代码POST /api/tag/link_entity |
Response:
1 | 复制代码{ |
查询实体关联的标签列表
Request:
1 | 复制代码GET /api/tag/entity_tags |
Response:
1 | 复制代码{ |
编码实现
初始化:
1 | 复制代码mkdir tag-server |
安装将要用到依赖项:
1 | 复制代码go get github.com/go-sql-driver/mysql github.com/jmoiron/sqlx github.com/gin-gonic/gin github.com/elastic/go-elasticsearch/v7 |
创建 cmd/api-server/main.go 并编写脚手架代码:
1 | 复制代码package main |
实现创建标签的 API
连接数据库:
1 | 复制代码import "github.com/jmoiron/sqlx" |
定义 Tag 结构:
1 | 复制代码type Tag struct { |
编写创建标签的逻辑:
1 | 复制代码// NewTagReqBody 创建标签的请求体 |
启动测试一下:
1 | 复制代码go run cmd/api-server/main.go |
创建一个名为 test 的标签:
1 | 复制代码curl --request POST \ |
响应:
1 | 复制代码{ |
再创建一个叫做 测试 的标签:
1 | 复制代码curl --request POST \ |
响应:
1 | 复制代码{ |
重新运行一遍创建 test 标签的请求:
1 | 复制代码curl --request POST \ |
响应:
1 | 复制代码{ |
测试结果符合预期,当前完整文件内容如下:
1 | 复制代码package main |
实现搜索标签的 API
导入 elasticsearch 包:
1 | 复制代码import ( |
声明 esClient 变量:
1 | 复制代码var ( |
在 init 函数中初始化 esClient:
1 | 复制代码func init() { |
把标签添加至 ES 索引
为了能在 ES 上搜到标签,我们需要在添加标签的时候,把标签添加至 ES 索引中。
先修改 Tag 结构,增加 JSON Tag, 并添加转换成 JSON 字符串的方法:
1 | 复制代码// Tag 标签结构定义 |
然后添加一个上报 Tag 到 ES 索引的函数:
1 | 复制代码// ReportTagToES 上报 Tag 到 ES |
在 OnNewTag 函数的底部增加上报的逻辑:
1 | 复制代码func OnNewTag(c *gin.Context) { |
重新启动服务,然后测试创建 Tag,观察日志:
1 | 复制代码2020/06/05 11:29:11 ESIndexRequestOk: [201 Created] {"_index":"test","_type":"tag","_id":"4","_version":1,"result":"created","forced_refresh":true,"_shards":{"total":2,"successful":1,"failed":0},"_seq_no":3,"_primary_term":1} |
再调用 ES 的 API 验证一下:
1 | 复制代码curl -XGET "localhost:9200/test/tag/4" |
完善搜索逻辑
新增一个 SearchTagReqBody 结构,作为搜索标签的请求体
1 | 复制代码type SearchTagReqBody struct { |
在 OnSearchTag 函数里面增加一些基本的参数校验:
1 | 复制代码func OnSearchTag(c *gin.Context) { |
增加一个 O
结构作为 map[string]interface{}
的别名,并且为这个结构添加一个 MustToJSONBytesBuffer() *bytes.Buffer
的方法:
1 | 复制代码type O map[string]interface{} |
定义这个 O
是为了等会构建 ES 查询提供一点便利。
增加 SearchTagsFromES 函数,从 ES 上搜索 Tags:
1 | 复制代码func SearchTagsFromES(keyword string) ([]*Tag, error) { |
修改 OnSearchTag 函数,加入搜索的逻辑:
1 | 复制代码func OnSearchTag(c *gin.Context) { |
重新启动服务,然后添加一个美食标签,然后再搜索:
1 | 复制代码curl --request GET \ |
搜索 API 最终效果
先清空一下 MySQL 的历史数据,之前添加标签的时候,还没有添加到 ES 的索引里面:
1 | 复制代码truncate tag_tbl; |
同时也清理一下 ES 索引:
1 | 复制代码curl -XDELETE "localhost:9200/test" |
接下来添加一批 Tag:
1 | 复制代码美食 |
搜索 “美食”:
1 | 复制代码{ |
搜索 “美食街”:
1 | 复制代码{ |
搜索 “美食节”:
1 | 复制代码{ |
实现关联标签到实体 API
定义实体关联 Tag 的结构:
1 | 复制代码type EntityTag struct { |
定义请求体:
1 | 复制代码type LinkEntityReqBody struct { |
开始编写 OnLinkEntity 里面的逻辑,首先先做基本的参数校验:
1 | 复制代码var reqBody LinkEntityReqBody |
查询是否标签已经关联过该实体,如果已经关联过,则直接返回:
1 | 复制代码var entityTag EntityTag |
判断一下 Tag 是否存在:
1 | 复制代码var tag Tag |
记录关联信息并返回关联 ID:
1 | 复制代码execResult, execErr := mysqlDB.Exec( |
重启服务,创建一些关联:
1 | 复制代码curl --request POST \ |
可以通过数据库来验证一下:
1 | 复制代码mysql> select * from entity_tag_tbl; |
实现查询实体关联的标签列表 API
定义查询实体关联的标签列表的请求体:
1 | 复制代码type EntityTagReqBody struct { |
编写 OnEntityTags 逻辑,和之前一样做参数校验:
1 | 复制代码var reqBody EntityTagReqBody |
查询出实体关联的标签:
1 | 复制代码entityTags := []*EntityTag{} |
查询出标签列表,并返回:
1 | 复制代码tagIDs := make([]int, 0, len(entityTags)) |
重启服务测试一下:
1 | 复制代码curl --request GET \ |
最后
完整的代码可以在 Github 上找到:
参考资料:
本文转载自: 掘金