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

Posted 小象裤衩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go项目优化——使用Elasticsearch搜索引擎相关的知识,希望对你有一定的参考价值。

本文为通过实例(图书项目)来学习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自行进行压力测试。

以上是关于Go项目优化——使用Elasticsearch搜索引擎的主要内容,如果未能解决你的问题,请参考以下文章

Elasticsearch:运用 Go 语言实现 Elasticsearch 搜索 - 8.x

GO语言操作Elasticsearch

个推Elasticsearch集群中JVM问题的应对之策

《Elasticsearch 源码解析与优化实战》第19章:搜索速度优化

《Elasticsearch 源码解析与优化实战》第19章:搜索速度优化

亿级 Elasticsearch 性能优化