自制信息检索网站(三)——Go语言标准库实现后端

679 阅读6分钟

前两篇博客介绍到了掘金数据的爬取和分析,而我们的最终目的是搭建一个小型的检索系统。当然数据全都是偷掘金的了(笑)。目前这个项目已经搭建完毕了,已经发布到github上面了juejin,另外我也之前做了一个伯乐在线的小型检索系统,地址在这demo

下面就进入今天的正题,关于如何使用Go语言搭建一个简单的后台项目。由于在信息搜索上面使用到了ElasticSearch这个非常棒的开源项目,所以也使用了Go语言版的elastic实现了内容搜索,这也是这个后台项目唯一的第三方依赖,其它都为Go语言标准库实现的。项目结构入如下图所示:

在static文件夹下是前端的资源部分,view文件夹下是前端视图部分,juejin文件夹下是后台项目的依赖,main.go文件是整个项目的入口,接下来让我们由浅入深的看一下整个项目。

utils

因为这个项目的场景不大单纯的只是涉及了内容检索以及资源服务,所以utils这里面的东西并没有太多,而由于Go语言的特性,它会经常涉及error的检查,所以整个utils.go里面也就只有关于error的处理的两个方法。

// print the error
func ErrorPrint(err error) bool {
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

第一个方法,由于在使用elastic查询内容时不可避免的会出现error,可是又不能让程序crash掉,于是便有了这个ErrorPrint方法。

// fatal the error then the program will be broken
func ErrorFatal(err error) {
	if err != nil {
		log.Fatal(err)
		return
	}
}

第二个方法,是用于程序出现了严重错误,没有必要在继续运行下去所定义的方法,对于严重的error如在http在ListenAndServe时出错,便可以直接Fatal。

log

顾名思义,这个log.go里面实现了日志打印的功能,对于检测程序运行情况,以及查看请求的信息,一个简单的日志打印功能是十分有必要的。

func WriteLog(r *http.Request, t time.Time, match string, pattern string) {
	d := time.Now().Sub(t)

	l := fmt.Sprintf("[ACCESS] | % -10s | % -40s | % -16s | % -10s | % -40s |", Bold(Blue(r.Method)), r.URL.Path, d.String(), Red(match), Green(pattern))

	log.Println(l)
}

这个函数会对请求的路径和方法,以及响应时间,匹配路径进行日志输出。至于里面的Red(),Green(),Bold()均是为了改变在终端里面的字体颜色,让整个日志信息更为清晰。效果图如下:

elastic

前面也说到,整个项目会不断的涉及文章检索,内容推荐的操作,而这些便都需要使用elasticsearch来完成,因此笔者把所有关于与elasticsearch交互的功能写在了这个文件里面,其实它也很简单,只是接收请求的参数并将其结构化用来请求elasticsearch。首先看这个getItems函数。

// get the article items from the elastic server
func getItems(keyword string, page int) *elastic.SearchHits {
	// search result from the tags,title,content of the article item
	query := elastic.NewMultiMatchQuery(keyword, "tags", "title", "content")
	result, err := client.Search().
			Index("juejin").
			Query(query).
			From((page - 1) * 10).Size(10).
			Do(ctx)
	if ok := ErrorPrint(err); ok {
		return result.Hits
	}
	return nil
}

这个函数接收两个参数keyword和page,即关键词和页数。然后将它们写入elasticsearch的复合查询中,并将hits结果返回。实际的结果如下图:

另外还有一个getSuggestions函数主要用来做搜索框的提示功能,具体实现大同小异,这里边不同阐述。

router

在router.go里面首先定义了一个结构体:

type Controller struct {
	HandleFunction func(http.ResponseWriter, *http.Request)
	Method         string
	Pattern        string
}

这个结构体里面有三个成员,HandleFunction用来处理请求的响应函数,Method用来装填请求方法,Pattern用来匹配请求的路径,这里使用了正则匹配。

func GetItems(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	keyword := r.Form.Get("keyword")
	page_str := r.Form.Get("page")
	if page_str == "" {
		// w.WriteHeader(403)
		page_str = "1"
	}
	page, err := strconv.Atoi(page_str)
	if ok := ErrorPrint(err); ok {
		w.Header().Set("Content-Type", "application/json")
		items := getItems(keyword, page)
		if items != nil {
			data, err := json.Marshal(items)
			ok = ErrorPrint(err)
			if ok {
				w.Write(data)
			}
		}
	}
	w.Write([]byte(""))
}

上面的代码是router.go里面的一个方法,主要用来响应网页对于文章内容的请求,效果图便是上面的效果图了。这个函数首先会调用ParseForm方法解析请求的参数,由于elastic的在page上不能使用空值,因此如果接受到的page参数为空的话会将其处理成1,然后在Header里将返回的数据类型设置为json,并且调用elastic.go里面的getItems函数,通过json的编组函数Marshal得到json格式的数据并返回。router.go里面的其它方法也是如此实现的。

main

由于本次使用的是Go语言http.Server这个结构体实现的服务器,因此就必须自定义一个自己的HttpHandler并实现ServeHTTP这个方法。

type httpHandler struct{}

func (*httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	t := time.Now()
	for _, controller := range mux {
		if ok, _ := regexp.MatchString(controller.Pattern, r.URL.Path); ok {
			if r.Method == controller.Method {
				controller.HandleFunction(w, r)
				go juejin.WriteLog(r, t, "matched", controller.Pattern)
				return
			}
		}
	}
	go juejin.WriteLog(r, t, "unmatch", r.URL.Path)
	return
}

这个结构体会传到Server.Handler这个参数上,它来实现整个的路由调配。首先它会得到当前的时间t,然后遍历mux这个切片。

func init() {
	mux = append(mux, juejin.Controller{juejin.GetItems, "GET", "^/api/getItems"})
	mux = append(mux, juejin.Controller{juejin.GetSuggestions, "GET", "^/api/getSuggestions"})
	mux = append(mux, juejin.Controller{Static, "GET", "^/static/"})
	mux = append(mux, juejin.Controller{DefaultPage, "GET", "^/"})
}

mux这个切片里面存放着所有处理请求的方法。当用户请求的方法通过正则匹配到了我们的路径时便调用这个controller里面的方法即controller.HandleFunction(w, r),然后打印一条日志,如果不匹配,则打印不匹配的日志。

另外mux里面前两个都是数据请求的处理,Static这个方法用于处理静态文件,具体如下:

var wd, _ = os.Getwd()

func Static(w http.ResponseWriter, r *http.Request) {
	path := filepath.Join(wd, r.URL.Path)
	http.ServeFile(w, r, path)
}

我们通过os.Getwd获得当前工作路径,然后通过请求的资源路径并且将这个静态资源通过http.ServeFile方法返回给用户这个资源。

DefaultPage这个方法用于服务于默认的请求页面。

func DefaultPage(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles(filepath.Join(wd, "view/index.html"))
	if err != nil {
		w.WriteHeader(403)
		return
	}
	err = tmpl.Execute(w, nil)
	if err != nil {
		w.WriteHeader(403)
		return
	}
}

这个方法通过html/template这个默认引擎来处理我们的index.html页面,当用户请求时便处理这个模板,然后写入到ResponseWriter中。另外由于我们使用的是正则匹配,并且通过for循环来遍历匹配请求路径,因此必须将"^/"这个路径放在切片的最末尾,否则其它所有请求都会被它匹配,而一旦被它匹配到便会调用DefaltPage这个Controller,其它的Controller便不会调用,整个项目也无法正常运行。好了,由于最近一直在学习Go语言,所以做了项目练手,但确实对路由架构这方面不熟悉,所以整个路由结构非常凌乱。如果您有什么建议,请不吝赐教。