[关闭]
@zwh8800 2017-08-20T15:21:20.000000Z 字数 5841 阅读 312689

用 elasticsearch 给博客加上了搜索

blog elasticsearch golang 搜索引擎


博客从 Wordpress 迁移过来之后一直缺少一个搜索功能,这个博客我是当做笔记性质的,有时候脑子里突然想不起某个东西的时候就上来查一下。没有搜索还是很不方便的,所以费了点时间研究了下大名鼎鼎的 elasticsearch 配合 golang 给博客加上了搜索功能。


elasticsearch 介绍

elasticsearch 是一个 java 编写的搜索和分析引擎,功能十分强大。但是并不意味着你的程序必须使用 java 开发,elasticsearch 是一个独立运行的程序,它会开放一个 RESTful 的接口供人调用,所以使用起来十分方便,甚至使用 curl 就能对它进行访问。另外,elasticsearch 的可伸缩性也很吸引我,使用 elasticsearch 组建一个集群十分方便,只需要把几个 elasticsearch 放到同一个局域网内就可以了,不用做任何配置你就能跑起来一个集群。这样,当你的数据量或者并发量增大的时候,只需要简单的购买几台新服务器就能解决性能问题。

我是通过 Elasticsearch 权威指南(中文版) 这本书来学习的,也推荐大家看一看,比我讲的好。

一些概念

elasticsearch 中有几个基本概念,大概可以和数据库的这几个概念对应起来(如下表)。但是有一点需要注意,elasticsearch 中不会限制数据必须存在一个二维表中,你可以保存一个对象,一个数组,一个字符串,或者一个整数,就像一个 JSON 一样,十分灵活。事实上,elasticsearch 的通讯协议确实是使用 JSON 的。

数据库 elasticsearch
Databases 索引(Indices)
Tables 类型(Types)
Rows 文档(Documents)
Columns 字段(Fields)
schema Mapping

在 elasticsearch 中保存的每条记录叫一个 document ,它可以是一个包含很多字段的对象,默认情况下每个字段都能被搜索。

基本操作

使用 curl 就可以对 elasticsearch 进行操作,但是我还是推荐一个 chrome 应用 postman ,有 JSON 语法高亮和检测,还可以保存历史记录。

90.pic_hd.jpg-1070kB

索引一条记录

在 elasticsearch 中存储数据的行为叫做 索引(index) 。使用 HTTP 协议的 PUT 动词可以存储数据。

  1. PUT http://localhost:9200/mdblog/note/23432
  2. {
  3. "id": 23,
  4. "notename": "golang-china-download-mirror",
  5. "title": "做了个 golang 安装包的镜像",
  6. "content": "做了个 golang 安装包的镜像 闲扯 golang\n \n 2016-05-25 16:04 PM\n 鉴于国情,国内下载 golang 安装包还是挺蛋疼的,就算使用代理速度也比较感人。虽然现在 docker 镜像是个比较好的选择,但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ,定时拉取 golang 官网的安装包到我的服务器上。地址在这里:https://lengzzz.com/download/golang/包含了 golang 1.5 之后的所有版本,所有平台的安装包和源码包都放在里面,自行 control + f 搜一下吧。新版本的 golang release 之后,应该在一两天内可以拉取过来。欢迎使用。",
  7. "timestamp": "2016-05-25T08:04:53Z",
  8. "lastModified": "2016-05-25T09:01:42.923822162Z",
  9. "tagList": [
  10. "golang",
  11. "闲扯"
  12. ]
  13. }

如上,把要存储的数据写成一个 JSON 对象,放到 HTTP 的 Body 中传送给 elasticsearch 即可存储数据。

我们可以看到 url 中包含了 4 部分的信息。

名字 信息
localhost:9200 Elasticsearch 的 url
mdblog 索引名(Index)
note 类型名(Type)
23432 文档ID(Document ID)

很方便吧。

获取一条记录

大家应当已经想到了,使用 GET 动词。

  1. GET http://localhost:9200/mdblog/note/23432

返回的信息会多一些 metadata 。

  1. {
  2. "_index": "mdblog",
  3. "_type": "note",
  4. "_id": "389344",
  5. "_version": 1,
  6. "found": true,
  7. "_source": {
  8. "id": 23,
  9. "notename": "golang-china-download-mirror",
  10. "title": "做了个 golang 安装包的镜像",
  11. "content": "做了个 golang 安装包的镜像 闲扯 golang\n \n 2016-05-25 16:04 PM\n 鉴于国情,国内下载 golang 安装包还是挺蛋疼的,就算使用代理速度也比较感人。虽然现在 docker 镜像是个比较好的选择,但还是有很多场景需要原始的 golang 环境的。所以抽空做了个 mirror ,定时拉取 golang 官网的安装包到我的服务器上。地址在这里:https://lengzzz.com/download/golang/包含了 golang 1.5 之后的所有版本,所有平台的安装包和源码包都放在里面,自行 control + f 搜一下吧。新版本的 golang release 之后,应该在一两天内可以拉取过来。欢迎使用。",
  12. "timestamp": "2016-05-25T08:04:53Z",
  13. "lastModified": "2016-05-25T09:01:42.923822162Z",
  14. "tagList": [
  15. "golang",
  16. "闲扯"
  17. ]
  18. }
  19. }

搜索

搜索的话可以使用 查询 DSL 进行,说是 DSL(领域特定语言) 听起来很吓人,实际上就是几个 JSON 对象的组合而已。

调用搜索接口需要在 url 后面加一个 _search

  1. GET http://localhost:9200/mdblog/note/_search
  2. {
  3. "query" : {
  4. "match" : {
  5. "title" : "linux"
  6. }
  7. }
  8. }

这样,就可以使用 match 查询进行查询了。

结果:

  1. {
  2. "took": 2,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 5,
  6. "successful": 5,
  7. "failed": 0
  8. },
  9. "hits": {
  10. "total": 3,
  11. "max_score": 0.6609862,
  12. "hits": [
  13. {
  14. "_index": "mdblog",
  15. "_type": "note",
  16. "_id": "340165",
  17. "_score": 0.6609862,
  18. "_source": {
  19. "id": 11,
  20. "notename": "add-swap-on-linux",
  21. "title": "在Linux下设置swap",
  22. "content": "在Linux下设置swap linux\n \n 2016-04-10 15:53 PM\n 今早起来发现博客的数据库挂了,赶紧用手机上的ConnectBot连上去把mysql启动。看了下日志大概是因为内存不够用且没设置swap,所以mysql进程申请不到内存挂了(小内存服务器桑不起)所以赶紧把swap搞上,这样至少能让服务不轻易挂掉。这里记录一下,以备遗忘。大概分三步\n生成一个空文件\n把文件格式化成swap格式\n挂载\n",
  23. "timestamp": "2016-04-10T07:53:58Z",
  24. "lastModified": "2016-05-24T05:27:01.435772194Z",
  25. "tagList": [
  26. "linux"
  27. ]
  28. }
  29. }
  30. ]
  31. }
  32. }

另外,我们可以为搜索加上高亮:

  1. GET http://localhost:9200/mdblog/note/_search
  2. {
  3. "query" : {
  4. "match" : {
  5. "title" : "linux"
  6. }
  7. },
  8. "highlight": {
  9. "pre_tags" : ["<b>"],
  10. "post_tags" : ["</b>"],
  11. "fields" : {
  12. "title" : {}
  13. }
  14. }
  15. }

这样的话,在 hit 中会有一个 highlight 字段,所有关键字会用 <b></b> 扩起来。

创建 mapping

默认情况下 elasticsearch 是不需要“建表”操作的。mapping(类似数据库的表结构)会在第一次 index 的时候建立。但是提前建立 mapping 有助于查询。建立 mapping 也是使用 PUT 动词。

  1. PUT http://localhost:9200/mdblog/note/_mapping
  2. {
  3. "note": {
  4. "properties": {
  5. "id": {
  6. "type": "long"
  7. },
  8. "title": {
  9. "type": "string",
  10. "term_vector": "with_positions_offsets",
  11. "analyzer": "ik_syno",
  12. "search_analyzer": "ik_syno"
  13. },
  14. "content": {
  15. "type": "string",
  16. "term_vector": "with_positions_offsets",
  17. "analyzer": "ik_syno",
  18. "search_analyzer": "ik_syno"
  19. },
  20. "notename": {
  21. "type": "string"
  22. },
  23. "tagList": {
  24. "type": "string",
  25. "term_vector": "with_positions_offsets",
  26. "analyzer": "ik_syno",
  27. "search_analyzer": "ik_syno"
  28. },
  29. "timestamp": {
  30. "type": "date",
  31. "index": "not_analyzed"
  32. },
  33. "lastModified": {
  34. "type": "date",
  35. "index": "not_analyzed"
  36. }
  37. }
  38. }
  39. }

如上,建立一个 mapping 。主要是设置一下数据类型和查询方式。


在 golang 中使用

在 golang 中有方便的 package 来操纵 elasticsearch。我使用的是 gopkg.in/olivere/elastic.v3 还不错的一个包,所有操作都是链式调用,很有 linq 的感觉。

使用 elastic 需要先创建一个客户端:

  1. func InitElasticSearch() (err error) {
  2. esClient, err = elastic.NewClient(
  3. elastic.SetURL("http://localhost:9200"))
  4. if err != nil {
  5. return err
  6. }
  7. return nil
  8. }

然后,就可以用 client 进行操作了。

  1. noteDetail := model.NoteDetail{
  2. Id: note.Id,
  3. Notename: note.Notename,
  4. Title: note.Title,
  5. Content: note.ContentText(),
  6. Timestamp: note.Timestamp,
  7. LastModified: note.LastModified,
  8. TagList: tagNameList,
  9. }
  10. // 首先调用 Index 函数,代表这是一次索引(Index)操作
  11. // 接着提供各种参数
  12. _, err := esClient.Index().
  13. Index(MdBlogIndexName).
  14. Type(NoteTypeName).
  15. Id(strconv.FormatInt(note.UniqueId, 10)).
  16. BodyJson(noteDetail).
  17. Do()
  18. if err != nil {
  19. return err
  20. }

索引一条记录

  1. func IsNoteDocumentExist(uniqueId int64) (bool, error) {
  2. return esClient.Exists().
  3. Index(MdBlogIndexName).
  4. Type(NoteTypeName).
  5. Id(strconv.FormatInt(uniqueId, 10)).
  6. Do()
  7. }

判断是否存在

  1. func SearchNoteByKeyword(keyword string,
  2. page, limit int64) ([]*model.SearchedNote, int64, error) {
  3. page-- //数据库层的页数从0开始数
  4. offset := page * limit
  5. query := elastic.NewMultiMatchQuery(keyword).
  6. FieldWithBoost("notename", 1).
  7. FieldWithBoost("tagList", 2).
  8. FieldWithBoost("content", 4).
  9. FieldWithBoost("title", 4)
  10. highlight := elastic.NewHighlight().
  11. Field("content").
  12. Field("title").
  13. Field("tagList")
  14. result, err := esClient.Search().
  15. Index(MdBlogIndexName).
  16. Type(NoteTypeName).
  17. Query(query).
  18. Highlight(highlight).
  19. From(int(offset)).
  20. Size(int(limit)).
  21. Do()
  22. if err != nil {
  23. return nil, 0, err
  24. }
  25. if result.Hits == nil {
  26. return nil, 0, nil
  27. }
  28. maxPage := (result.TotalHits()-1)/limit + 1
  29. noteList := make([]*model.SearchedNote, 0, len(result.Hits.Hits))
  30. for _, hit := range result.Hits.Hits {
  31. note := model.NewSearchedNote()
  32. err := json.Unmarshal(*hit.Source, note)
  33. if err != nil {
  34. return nil, 0, err
  35. }
  36. note.FillHighlight(hit.Highlight)
  37. noteList = append(noteList, note)
  38. }
  39. return noteList, maxPage, nil
  40. }

搜索记录


(´ ・ω・`)

总的来说,elasticsearch 还是很方便强大的,好评。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注