zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Go项目优化——使用Elasticsearch搜索引擎

Goelasticsearch项目 使用 优化 搜索引擎
2023-06-13 09:18:38 时间

本文为通过实例(图书项目)来学习go中Elasticsearch的使用,以及对项目带来的性能的提升

案例:

http准备

util/http.go 用于向es服务器发送json格式的Put和Post请求

package util

import (
	"errors"
	"github.com/astaxie/beego/httplib"
	"github.com/bitly/go-simplejson"
	"io"
	"time"
)

// HttpPutJson
// @Title HttpPutJson
// @Description 用于向es服务器发送put请求(新建索引or添加文档)
func HttpPutJson(url, body string) error {
	resp, err := httplib.Put(url).
		Header("Content-Type", "application/json").
		SetTimeout(10*time.Second, 10*time.Second).
		Body(body).
		Response()
	if err == nil {
		defer resp.Body.Close()
		// 不正常的响应状态码
		if resp.StatusCode >= 300 || resp.StatusCode < 200 {
			// es会将错误信息写在body里 打印错误信息
			bodyErr, _ := io.ReadAll(resp.Body)
			body = string(bodyErr)
			err = errors.New(resp.Status + ";" + body)
		}
	}
	return err
}


// HttpPostJson
// @Title HttpPostJson
// @Description 用于向es服务器请求数据,查询数据
// @Param url string
// @Param body string 条件
// @Return *simplejson.Json es服务器返回的信息
func HttpPostJson(url, body string) (*simplejson.Json, error) {
	resp, err := httplib.Post(url).
		Header("Content-Type", "application/json").
		SetTimeout(10*time.Second, 10*time.Second).
		Body(body).
		Response()
	var sj *simplejson.Json
	if err == nil {
		defer resp.Body.Close()
		// 不正常的响应状态码
		if resp.StatusCode >= 300 || resp.StatusCode < 200 {
			bodyErr, _ := io.ReadAll(resp.Body)
			body = string(bodyErr)
			err = errors.New(resp.Status + ";" + body)
		} else {
			bodyBytes, _ := io.ReadAll(resp.Body)
			sj, err = simplejson.NewJson(bodyBytes)
		}
	}
	return sj, err
}

案例(新增):

建立索引+添加文档 发布图书的时候为图书和章节文档内容建立索引。 models/elasticSearch.go

package models

import (
	"es.study/util"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/astaxie/beego/logs"
	"strconv"
	"strings"
)

var (
	// (应写在配置文件里)搜索引擎配置,后面要加'/'
	elasticHost = "http://localhost:9200/"
)

// ElasticBuildIndex
// localhost:9200/index/_doc/doc_id
// index: 索引 对应sql里的表
// _doc: 文档类型,ES 7.0 以后的版本 已经废弃文档类型了,一个 index 中只有一个默认的 type,即 _doc。
// @Title ElasticBuildIndex
// @Description  指定id的图书增加索引
// @Author hyy 2022-10-14 21:06:27
// @Param bookId int 图书 id
func ElasticBuildIndex(bookId int) {
	// func(m *Book) Select(filed string, value interface{}, cols ...string)
	// SELECT [cols...] FROM books WHERE filed=value;
	book, _ := NewBook().Select("book_id", bookId, "book_id", "book_name", "description")
	addBookToIndex(book.BookId, book.BookName, book.Description)

	// index document
	var documents []Document
	fields := []string{"document_id", "book_id", "document_name", "release"}
	GetOrm("r").QueryTable(TNDoucments()).Filter("book_id", bookId).All(documents, fields...)
	if len(documents) > 0 {
		for _, document := range documents {
			// release: 已发布的章节
			addDocumentToIndex(document.DocumentId, document.BookId, flatHtml(document.Release))
		}
	}

}

// addBookToIndex
// @Title addBookToIndex
// @Description 向图书索引(相当于图书表)中,添加图书
// @Author hyy 2022-10-14 21:07:38
// @Param bookId int 图书id
// @Param bookName string 图书名
// @Param description string 图书描述
func addBookToIndex(bookId int, bookName, description string) {
	queryJson := `
		{
			"book_id":%v,
			"book_name":"%v",
			"description":"%v"
		}
	`
	// ElasticSearch API
	host := elasticHost
	api := host + "mbooks/_doc/" + strconv.Itoa(bookId)
	// 发起请求:
	queryJson = fmt.Sprintf(queryJson, bookId, bookName, description)
	err := util.HttpPutJson(api, queryJson)
	if err != nil {
		logs.Debug(err)
	}
}

// addDocumentToIndex
// @Title addDocumentToIndex
// @Description  向章节文档索引(相当于章节文档表)中,添加章节文档
// @Author hyy 2022-10-14 21:09:09
// @Param documentId int 文档id
// @Param bookId int 所属图书id
// @Param release string 文档发布内容
func addDocumentToIndex(documentId, bookId int, release string) {
	queryJson := `
		{
			"document:_id":%v,
			"book_id":%v,
			"release":"%v"
		}
	`
	// ElasticSearch API
	host := elasticHost
	api := host + "mdocument/_doc/" + strconv.Itoa(documentId)

	// 发起请求:
	queryJson = fmt.Sprintf(queryJson, documentId, bookId, release)
	err := util.HttpPutJson(api, queryJson)
	if err != nil {
		logs.Debug(err)
	}
}

// flatHtml
// 剔除章节里的html标签,取出文本
func flatHtml(htmlStr string) string {
	htmlStr = strings.Replace(htmlStr, "\n", " ", -1)
	htmlStr = strings.Replace(htmlStr, "\"", "", -1)
	gq, err := goquery.NewDocumentFromReader(strings.NewReader(htmlStr))
	// 如果不为空,说明没有
	if err != nil {
		return htmlStr
	}
	return gq.Text()
}

在发布图书的时候,调用ElasticBuildIndex(bookId)接口,将图书信息以及章节内容添加到es中。

案例(查询):

搜索图书:

package models

import (
	"es.study/util"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/astaxie/beego/logs"
	"strconv"
	"strings"
)

var (
	// (应写在配置文件里)搜索引擎配置,后面要加'/'
	elasticHost = "http://localhost:9200/"
)

// ... 新增索引or添加文档

// ElasticSearchBook
// localhost:9200/index/_doc/_search
// index: 索引 对应sql里的表
// _doc: 文档类型,ES 7.0 以后的版本 已经废弃文档类型了,一个 index 中只有一个默认的 type,即 _doc。
// @Title ElasticSearchBook
// @Description 根据关键字搜索图书,获取图书的id
// @Author hyy 2022-10-14 20:49:37
// @Param kw string 关键字
// @Param pageSize int 页大小
// @Param page int 页码(可选)
// @Return []int bookId的数组
// @Return int 书的总数
// @Return error 错误
func ElasticSearchBook(kw string, pageSize, page int) ([]int, int, error) {
	var bookIds []int
	count := 0
	if page > 0 {
		// 第一页对应搜索引擎里的第0页
		page = page - 1
	} else {
		page = 0
	}
	queryJson := `
		{
			"query":{
				"multi_match":{
					"query":"%v",
					"fields":["bookName","description"]
				}
			},
			"_source":["book_id"],
			"size":%v,
			"from":%v
		}
	`

	// elasticSearch api
	host := elasticHost
	api := host + "mbook/_doc/_search"
	queryJson = fmt.Sprintf(queryJson, kw, pageSize, page)
	sj, err := util.HttpPostJson(api, queryJson)
	if err == nil {
		count = sj.GetPath("hits", "total").MustInt()
		resultArray := sj.GetPath("hits", "hits").MustArray()
		for _, result := range resultArray {
			if eachMap, ok := result.(map[string]interface{}); ok {
				id, _ := strconv.Atoi(eachMap["_id"].(string))
				bookIds = append(bookIds, id)
			}
		}
	}
	return bookIds, count, err
}

// ElasticSearchDocument
// @Title ElasticSearchDocument
// @Description 根据关键字搜索章节文档,返回章节文档的id,
// 该函数提供两种搜索:
//  1. 搜所有图书的章节文档
//  2. 搜某一本图书的章节文档,所以有个可选参数bookId
//
// @Author hyy 2022-10-14 20:56:54
// @Param kw string 关键字
// @Param pageSize int 页大小
// @Param page int 页码
// @Param bookId ...int 图书id(可选)
// @Return []int 章节文档的id数组
// @Return int 总数
// @Return error 错误
func ElasticSearchDocument(kw string, pageSize, page int, bookId ...int) ([]int, int, error) {
	var documentIds []int
	count := 0
	if page > 0 {
		// 第一页对应搜索引擎里的第0页
		page = page - 1
	} else {
		page = 0
	}
	queryJson := `
		{
			"query":{
				"match":{
					"release":"%v",
				}	
			},
			"_source":["document_id"],
			"size":%v,
			"from":%v
		}
	`
	queryJson = fmt.Sprintf(queryJson, kw, pageSize, page)

	if len(bookId) > 0 && bookId[0] > 0 {
		queryJson = `
			{
				"query":{
					"bool":{
						"filter":[{
							"term":{
								"book_id":%v
							}
						}],
						"must":{
							"multi_match":{
								"query":"%v",
								"fields":["release"]
							}
						}
					}	
				},
				"_source":["document_id"],
				"size":%v,
				"from":%v
			}
		`
		queryJson = fmt.Sprintf(queryJson, kw, pageSize, page)
	}
	// elasticSearch api
	host := elasticHost
	api := host + "mdocument/_doc/_search"
	sj, err := util.HttpPostJson(api, queryJson)
	if err==nil{
		count =  sj.GetPath("hits","total").MustInt()
		resultArray := sj.GetPath("hits", "hits").MustArray()
		for _, result := range resultArray {
			if eachMap, ok := result.(map[string]interface{}); ok {
				id, _ := strconv.Atoi(eachMap["_id"].(string))
				documentIds = append(documentIds, id)
			}
		}
	}
	return documentIds, count, err
}

关于sj.GetPath("hits","hits")原因如下: es查询到多个结果的时候,返回结果如下:

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
———>"hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
———————>"hits": [ // 原始数据
            {
                "_index": "shopping",
                "_type": "_doc",
                "_id": "TYu9pn8BfWqG58AR7Mzw",
                "_score": 1.0,
                "_source": {
                    "title": "小米手机",
                    "category": "小米",
                    "images": "http://xxx.com/xm.jpg",
                    "price": 3999.00
                }
            },
            ...
        ]
    }
}

结果:

优化前:

优化后:

性能的具体提升使用ab自行进行压力测试。