使用Go语言和Elasticsearch构建简易的职位搜索后端

使用 Go 語言結合 Elasticsearch 架設一個簡易求職網站的後端。

本文是 MyNavi Advent Calendar 2021 的第9篇文章。

本次文章的源代码已在GitHub上公开。

文件夹的结构如下所示。

.
├── README.md
├── batch
│   ├── LoadData.go
│   ├── go.mod
│   ├── go.sum
│   └── test_data.xml
├── docker-compose.yml
├── es
│   ├── dic
│   │   └── test_dic.csv
│   ├── Dockerfile
│   ├── script
│   │   └── es_init.sh
│   └── sudachi
│       └── sudachi.json
├── search_api
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   ├── hr_api
│   ├── internal
│   │   ├── connect_es.go
│   │   ├── hr_query.go
│   │   └── hr_search.go
│   └── main.go

首先

用 Go 语言和 Elasticsearch 构建简易求职网站的后端。

在Docker容器上运行搜索引擎(Elasticsearch),然后从Go编写的Web服务器(echo)发送单词搜索查询。一旦进行单词搜索,我们将构建一个简单的职位搜索后端,以JSON格式输出职位信息。

构建时假设您发出以下查询来进行搜索。 shí nǐ xià de .)

 #東京都の"カフェ"の求人を検索する
http://localhost:5000/search?keyword=カフェ&state=東京都

 #東京都の"Go言語"の求人を検索する
http://localhost:5000/search?keyword=Go言語&state=東京都

 #神奈川県の"アルバイト・パート"の求人を検索する
http://localhost:5000/search?keyword=アルバイト・パート&state=神奈川県

 #求人のユニークidから検索する
http://localhost:5000/search?id=test

关键词用来指定搜索词,状态用来指定位置,id用作查询参数来指定招聘的唯一编号。

使用Kibana作为Elasticsearch的仪表盘。在Kibana中查看Elasticsearch的数据如下所示。

此外,我们还会创建一个批处理程序,将大量的XML数据转换成JSON格式,并将其输入到ElasticSearch中。
预计在生产环境中,我们每天运行一次该批处理程序以更新职位数据。
本次我们将简单地使用Go语言将大约10万条数据批量插入到ElasticSearch中。

image.png

操作系统是Ubuntu 20.04。
以下是工具的版本。

ツールバージョンGo 言語1.17.2Docker1.41docker-compose1.29.2Elasticsearch7.8.1Kibana7.8.1

以下是制作步骤,请根据我记住的范围写下来,可能会有些前后顺序错乱,但希望您能参考一下。

1. 建立Elasticsearch

首先,我们建立一个Elasticsearch。

当查看 Elasticsearch 的版本公式时,发现有多个版本可供选择。
截至 2021 年 12 月 7 日,最新版本为 7.14.2。

选择7.8.1的理由有几个。最主要的原因是我们想要将Sudachi用作Elasticsearch的词典。在WorksApplication的存储库中,他们只支持到7.4,因此我们选择了7.4.1。

如果不一定非要使用辞书的话,我认为其他版本也可以运行。

最終的使用 Sudachi 的 Elasticsearch 的 Dockerfile 就是这样的。
由于不太了解 Docker 容器的最佳创建方式,可能对您并不有用。
我一直在尝试摸索如何减小容器镜像的大小,但最终并没有太大的改变,让我很苦恼。

ARG ELASTIC_VER=7.8.1
ARG SUDACHI_PLUGIN_VER=2.0.3

FROM ibmjava:8-jre-alpine as dict_builder

ARG ELASTIC_VER
ARG SUDACHI_PLUGIN_VER

WORKDIR /home

RUN wget https://github.com/WorksApplications/Elasticsearch-sudachi/releases/download/v${ELASTIC_VER}-${SUDACHI_PLUGIN_VER}/analysis-sudachi-${ELASTIC_VER}-${SUDACHI_PLUGIN_VER}.zip && \
    unzip analysis-sudachi-${ELASTIC_VER}-${SUDACHI_PLUGIN_VER}.zip && \
    wget http://sudachi.s3-website-ap-northeast-1.amazonaws.com/sudachidict/sudachi-dictionary-20210802-core.zip && \
    unzip sudachi-dictionary-20210802-core.zip && \
    mkdir -p /usr/share/Elasticsearch/config/sudachi/ && \
    mv sudachi-dictionary-20210802/system_core.dic /usr/share/Elasticsearch/config/sudachi/ && \
    rm -rf sudachi-dictionary-20210802-core.zip sudachi-dictionary-20210802/


FROM docker.elastic.co/Elasticsearch/Elasticsearch:${ELASTIC_VER}

ARG ELASTIC_VER
ARG SUDACHI_PLUGIN_VER

COPY es/sudachi/sudachi.json /usr/share/Elasticsearch/config/sudachi/
COPY --from=dict_builder /home/analysis-sudachi-${ELASTIC_VER}-${SUDACHI_PLUGIN_VER}.zip /usr/share/Elasticsearch/

可以从docker-compose.yml文件中提取出Elasticsearch部分,大致如下。
如果您能查看本文的GitHub源代码,将不胜感激。

  Elasticsearch:
    build:
      context: .
      dockerfile: es/dockerfile
    container_name: Elasticsearch
    volumes:
      - es-data:/usr/share/Elasticsearch/data
    networks:
      - Elasticsearch
    ports:
      - 9200:9200
    environment:
      - discovery.type=single-node
      - node.name=Elasticsearch
      - cluster.name=go-Elasticsearch-docker-cluster
      - bootstrap.memory_lock=true
      - ES_JAVA_OPTS=-Xms256m -Xmx256m
    ulimits:
      { nofile: { soft: 65535, hard: 65535 }, memlock: { soft: -1, hard: -1 } }
    healthcheck:
      test: curl --head --max-time 120 --retry 120 --retry-delay 1 --show-error --silent http://localhost:9200

我在docker-compose.yml文件中注意到的问题是确保后端服务器可以进行搜索,并处理内存限制的相关问题。

稍后我会稍微提一下,在进行BulkInsert时遇到了一些问题。

请参考官方文件以获取详细信息。

构建Kibana

因为按照官方文档进行了Kibana的构建,所以它能够正常运行,我没有遇到任何困扰。

最后,docker-compose.yml变成了以下的样子。

  kibana:
    container_name: kibana
    image: docker.elastic.co/kibana/kibana:7.8.1
    depends_on: ["Elasticsearch"]
    networks:
      - Elasticsearch
    ports:
      - 5601:5601
    environment:
      - Elasticsearch_HOSTS=http://Elasticsearch:9200
      - KIBANA_LOGGING_QUIET=true
    healthcheck:
      test: curl --max-time 120 --retry 120 --retry-delay 1 --show-error --silent http://localhost:5601

将数据插入到 Elasticsearch 数据库中。

我曾经在这里为了通过 Go 与 Elasticsearch 进行通信而苦恼。
首先,我纠结于选择使用哪个软件包。

一般来说,我认为以下两个软件包的使用较为广泛。

    1. https://github.com/olivere/elastic (弹性搜索)

 

    https://github.com/elastic/go-Elasticsearch (弹性搜索的 GO 语言版本)

1是Go语言中最受欢迎且Star数量最多的Elasticsearch客户端包。非常易用且文档齐全。一开始我们就打算使用这个来构建。

因为 Elastic 是官方提供的包,所以我决定这次使用2来创建。
由于文档并不是很完善,我参考了 GitHub 官方的_example 来创建。
一开始确实比较费力,但是熟悉之后发现有很多非常方便的功能。
这是一个需要花时间去适应的包。

批量插入

考虑到本次计划在 Elasticsearch 中输入 30 万条招聘数据,我之前创建时就假设需要使用 BulkInsert(go-Elasticsearch 中称之为 BulkIndex)来完成。

首先,我們使用普通的插入方法進行了創建。
當提取 Go 語言代碼時,大致如下。

req := esapi.IndexRequest{
                Index:      "baito",
                DocumentID: string(j.Referencenumber),
                Body:       strings.NewReader(string(jobbody)),
                Refresh:    "true",
            }

我根据GitHub上的公式参考进行了创建,但将大约10万条记录插入花费了大约1小时30分钟(由于遗失了测量照片)。
通常情况下,由于无法承受所有的插入,很容易在中途超时,因此我认为使用普通的插入方式无法实际应用30万条记录。

所以,我参考了BulkInsert,但是不太懂…(*´-ω・)ン? (。´-_・)ン? (´・ω・`)モキュ?

最终,我花了三天时间来理解这个文档(以及XML解析文档)。

最终完成的 Go 语言代码如下所示。

package main

import (
    "bytes"
    "encoding/json"
    "encoding/xml"
    "flag"
    "fmt"
    "io"
    "log"
    "math/rand"
    "os"
    "strings"
    "time"

    "github.com/dustin/go-humanize"
    "github.com/elastic/go-Elasticsearch/v7"
    "github.com/elastic/go-Elasticsearch/v7/esapi"
    "github.com/joho/godotenv"
)

type Job struct {
    Referencenumber string `xml:"referencenumber" json:"referencenumber,string"`
    Date            string `xml:"date" json:"date,string"`
    Url             string `xml:"url" json:"url,string"`
    Title           string `xml:"title" json:"title,string"`
    Description     string `xml:"description" json:"description,string"`
    State           string `xml:"state" json:"state,string"`
    City            string `xml:"city" json:"city,string"`
    Country         string `xml:"country" json:"country,string"`
    Station         string `xml:"station" json:"station,string"`
    Jobtype         string `xml:"jobtype" json:"jobtype,string"`
    Salary          string `xml:"salary" json:"salary,string"`
    Category        string `xml:"category" json:"category,string"`
    ImageUrls       string `xml:"imageUrls" json:"imageurls,string"`
    Timeshift       string `xml:"timeshift" json:"timeshift,string"`
    Subwayaccess    string `xml:"subwayaccess" json:"subwayaccess,string"`
    Keywords        string `xml:"keywords" json:"keywords,string"`
}

var (
    _     = fmt.Print
    count int
    batch int
)

func init() {
    flag.IntVar(&count, "count", 300000, "Number of documents to generate")
    flag.IntVar(&batch, "batch", 1000, "Number of documents to send in one batch")
    flag.Parse()

    rand.Seed(time.Now().UnixNano())
}

func main() {


    log.SetFlags(0)

        type bulkResponse struct {
        Errors bool `json:"errors"`
        Items  []struct {
            Index struct {
                ID     string `json:"_id"`
                Result string `json:"result"`
                Status int    `json:"status"`
                Error  struct {
                    Type   string `json:"type"`
                    Reason string `json:"reason"`
                    Cause  struct {
                        Type   string `json:"type"`
                        Reason string `json:"reason"`
                    } `json:"caused_by"`
                } `json:"error"`
            } `json:"index"`
        } `json:"items"`
    }

        var (
        buf bytes.Buffer
        res *esapi.Response
        err error
        raw map[string]interface{}
        blk *bulkResponse

        jobs  []*Job
        indexName = "baito"

        numItems   int
        numErrors  int
        numIndexed int
        numBatches int
        currBatch  int
    )

    log.Printf(
    "\x1b[1mBulk\x1b[0m: documents [%s] batch size [%s]",
    humanize.Comma(int64(count)), humanize.Comma(int64(batch)))
    log.Println(strings.Repeat("▁", 65))

    // Create the Elasticsearch client
    //
    es, err := Elasticsearch.NewDefaultClient()
    if err != nil {
        log.Fatalf("Error creating the client: %s", err)
    }

    err = godotenv.Load(".env")
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    xml_path := os.Getenv("BAITO_XML_PATH")
    f, err := os.Open(xml_path)

    if err != nil {
        log.Fatal(err)
    }

    defer f.Close()

    d := xml.NewDecoder(f)

    for i := 1; i < count+1; i++ {
        t, tokenErr := d.Token()
        if tokenErr != nil {
            if tokenErr == io.EOF {
                break
            }
            // handle error somehow
            log.Fatalf("Error decoding token: %s", tokenErr)
        }
        switch ty := t.(type) {
        case xml.StartElement:
            if ty.Name.Local == "job" {
                // If this is a start element named "location", parse this element
                // fully.
                var job Job
                if err = d.DecodeElement(&job, &ty); err != nil {
                    log.Fatalf("Error decoding item: %s", err)
                } else {
                    jobs = append(jobs, &job)
                }
            }
        default:
        }
        // fmt.Println("count =", count)
    }
    log.Printf("→ Generated %s articles", humanize.Comma(int64(len(jobs))))
    fmt.Print("→ Sending batch ")

        // Re-create the index
    //
    if res, err = es.Indices.Delete([]string{indexName}); err != nil {
        log.Fatalf("Cannot delete index: %s", err)
    }
    res, err = es.Indices.Create(indexName)
    if err != nil {
        log.Fatalf("Cannot create index: %s", err)
    }
    if res.IsError() {
        log.Fatalf("Cannot create index: %s", res)
    }

    if count%batch == 0 {
        numBatches = (count / batch)
    } else {
        numBatches = (count / batch) + 1
    }

    start := time.Now().UTC()

    // Loop over the collection
    //
    for i, a := range jobs {
        numItems++

        currBatch = i / batch
        if i == count-1 {
            currBatch++
        }

        // Prepare the metadata payload
        //
        meta := []byte(fmt.Sprintf(`{ "index" : { "_id" : "%d" } }%s`, a.Referencenumber, "\n"))
        // fmt.Printf("%s", meta) // <-- Uncomment to see the payload

        // Prepare the data payload: encode article to JSON
        //
        data, err := json.Marshal(a)
        if err != nil {
            log.Fatalf("Cannot encode article %d: %s", a.Referencenumber, err)
        }

        // Append newline to the data payload
        //
        data = append(data, "\n"...) // <-- Comment out to trigger failure for batch
        // fmt.Printf("%s", data) // <-- Uncomment to see the payload

        // // Uncomment next block to trigger indexing errors -->
        // if a.ID == 11 || a.ID == 101 {
        //  data = []byte(`{"published" : "INCORRECT"}` + "\n")
        // }
        // // <--------------------------------------------------

        // Append payloads to the buffer (ignoring write errors)
        //
        buf.Grow(len(meta) + len(data))
        buf.Write(meta)
        buf.Write(data)

        // When a threshold is reached, execute the Bulk() request with body from buffer
        //
        if i > 0 && i%batch == 0 || i == count-1 {
            fmt.Printf("[%d/%d] ", currBatch, numBatches)

            res, err = es.Bulk(bytes.NewReader(buf.Bytes()), es.Bulk.WithIndex(indexName))
            if err != nil {
                log.Fatalf("Failure indexing batch %d: %s", currBatch, err)
            }
            // If the whole request failed, print error and mark all documents as failed
            //
            if res.IsError() {
                numErrors += numItems
                if err := json.NewDecoder(res.Body).Decode(&raw); err != nil {
                    log.Fatalf("Failure to to parse response body: %s", err)
                } else {
                    log.Printf("  Error: [%d] %s: %s",
                        res.StatusCode,
                        raw["error"].(map[string]interface{})["type"],
                        raw["error"].(map[string]interface{})["reason"],
                    )
                }
                // A successful response might still contain errors for particular documents...
                //
            } else {
                if err := json.NewDecoder(res.Body).Decode(&blk); err != nil {
                    log.Fatalf("Failure to to parse response body: %s", err)
                } else {
                    for _, d := range blk.Items {
                        // ... so for any HTTP status above 201 ...
                        //
                        if d.Index.Status > 201 {
                            // ... increment the error counter ...
                            //
                            numErrors++

                            // ... and print the response status and error information ...
                            log.Printf("  Error: [%d]: %s: %s: %s: %s",
                                d.Index.Status,
                                d.Index.Error.Type,
                                d.Index.Error.Reason,
                                d.Index.Error.Cause.Type,
                                d.Index.Error.Cause.Reason,
                            )
                        } else {
                            // ... otherwise increase the success counter.
                            //
                            numIndexed++
                        }
                    }
                }
            }

            // Close the response body, to prevent reaching the limit for goroutines or file handles
            //
            res.Body.Close()

            // Reset the buffer and items counter
            //
            buf.Reset()
            numItems = 0
        }
    }

    // Report the results: number of indexed docs, number of errors, duration, indexing rate
    //
    fmt.Print("\n")
    log.Println(strings.Repeat("▔", 65))

    dur := time.Since(start)

    if numErrors > 0 {
        log.Fatalf(
            "Indexed [%s] documents with [%s] errors in %s (%s docs/sec)",
            humanize.Comma(int64(numIndexed)),
            humanize.Comma(int64(numErrors)),
            dur.Truncate(time.Millisecond),
            humanize.Comma(int64(1000.0/float64(dur/time.Millisecond)*float64(numIndexed))),
        )
    } else {
        log.Printf(
            "Sucessfuly indexed [%s] documents in %s (%s docs/sec)",
            humanize.Comma(int64(numIndexed)),
            dur.Truncate(time.Millisecond),
            humanize.Comma(int64(1000.0/float64(dur/time.Millisecond)*float64(numIndexed))),
        )
    }
}

我根据BulkInsert官方示例进行了研究,并创建了这个。

另外,由于我的电脑配置不算太高(4GB 内存,2个核心的CPU),因此XML解析器也必须要节省内存才行。

顺便提一句,写这段 Go 语言代码之前,我参考了 Python 的代码。到了一半的时候,我甚至考虑放弃 Go 语言。

从摘录到的内容来看,大致是这个样子。

for job in jobs:

    index = job.as_dict()
    if job.description == "" or job.description == null:
        continue

    bulk_file += json.dumps(
        {"index": {"_index": index_name, "_type": "_doc", "_id": id}}
    )

    # The optional_document portion of the bulk file
    bulk_file += "\n" + json.dumps(index) + "\n"

    if id % 1000 == 0:
        response = client.bulk(bulk_file)
        bulk_file = ""
        id += 1
        continue

    id += 1
if bulk_file != "":
    response = client.bulk(bulk_file)

我們將語言代碼和 Python 代碼分批進行 BulkInsert,每次處理 1000 條。

image.png

使用Go语言的代码,我们能够在约3分钟内将大约14万条数据插入到Elasticsearch。

4. 创建一个后端服务器

然后,我创建了从Go的Web服务器向Elasticsearch进行搜索的部分。
(在中途放弃了BulkInsert,先处理这个部分)。

虽然有很多 Go 的 Web 服务器可选择,但我们选择了简单的 echo。
它的文档也非常丰富,我们能够简洁地编写代码。

文件夹的结构如下所示。

.
├── search_api
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   ├── hr_api
│   ├── internal
│   │   ├── connect_es.go
│   │   ├── hr_query.go
│   │   └── hr_search.go
│   └── main.go

main.go只是一个简单的程序,使用echo框架搭建了一个Web服务器。

package main

import (
    internal "hr_api/internal"

    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
)

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())

    e.GET("/search", internal.HRSearch)

    e.Logger.Fatal(e.Start(":5000"))
}

内部的下属团队为此事经历了一番头痛后进行了创建。具体包括如何组织结构体(最终决定全部使用字符串;;;)以及如何与Elasticsearch进行通信等。

package internal

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/labstack/echo"
)

type Query struct {
    Keyword string `query:"keyword"`
    State   string `query:"state"`
    Id      string `query:"id"`
}

type Result struct {
    Referencenumber string `xml:"referencenumber" json:"referencenumber,string"`
    Date            string `xml:"date" json:"date,string"`
    Url             string `xml:"url" json:"url,string"`
    Title           string `xml:"title" json:"title,string"`
    Description     string `xml:"description" json:"description,string"`
    State           string `xml:"state" json:"state,string"`
    City            string `xml:"city" json:"city,string"`
    Country         string `xml:"country" json:"country,string"`
    Station         string `xml:"station" json:"station,string"`
    Jobtype         string `xml:"jobtype" json:"jobtype,string"`
    Salary          string `xml:"salary" json:"salary,string"`
    Category        string `xml:"category" json:"category,string"`
    ImageUrls       string `xml:"imageUrls" json:"imageurls,string"`
    Timeshift       string `xml:"timeshift" json:"timeshift,string"`
    Subwayaccess    string `xml:"subwayaccess" json:"subwayaccess,string"`
    Keywords        string `xml:"keywords" json:"keywords,string"`
}
type Response struct {
    Message string `json:"message"`
    Results []Result
}

func HRSearch(c echo.Context) (err error) {
    // クライアントからのパラメーターを取得
    q := new(Query)
    if err = c.Bind(q); err != nil {
        return
    }

    res := new(Response)
    var (
        b   map[string]interface{}
        buf bytes.Buffer
    )

    // Elasticsearch へのクエリを作成
    query := CreateQuery(q)

    json.NewEncoder(&buf).Encode(query)

    fmt.Printf(buf.String())

    // Elasticsearch へ接続
    es, err := ConnectElasticsearch()
    if err != nil {
        c.Error(err)
    }

    // Elasticsearch へクエリ
    r, err := es.Search(
        es.Search.WithContext(context.Background()),
        es.Search.WithIndex("baito"),
        es.Search.WithBody(&buf),
        es.Search.WithTrackTotalHits(true),
        es.Search.WithPretty(),
    )
    if err != nil {
        c.Error(err)
    }
    defer r.Body.Close()

    if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
        c.Error(err)
    }

    // クエリの結果を Responce.Results に格納
    for _, hit := range b["hits"].(map[string]interface{})["hits"].([]interface{}) {
        result := new(Result)
        doc := hit.(map[string]interface{})

        fmt.Printf(result.Title)

        result.Referencenumber = doc["_source"].(map[string]interface{})["referencenumber"].(string)
        result.Date = doc["_source"].(map[string]interface{})["date"].(string)
        result.Url = doc["_source"].(map[string]interface{})["url"].(string)
        result.Title = doc["_source"].(map[string]interface{})["title"].(string)
        result.State = doc["_source"].(map[string]interface{})["state"].(string)
        result.Category = doc["_source"].(map[string]interface{})["category"].(string)
        result.Description = doc["_source"].(map[string]interface{})["description"].(string)
        result.City = doc["_source"].(map[string]interface{})["city"].(string)
        result.Country = doc["_source"].(map[string]interface{})["country"].(string)
        result.Station = doc["_source"].(map[string]interface{})["station"].(string)
        result.Jobtype = doc["_source"].(map[string]interface{})["jobtype"].(string)
        result.Salary = doc["_source"].(map[string]interface{})["salary"].(string)
        result.ImageUrls = doc["_source"].(map[string]interface{})["imageurls"].(string)
        result.Timeshift = doc["_source"].(map[string]interface{})["timeshift"].(string)
        result.Subwayaccess = doc["_source"].(map[string]interface{})["subwayaccess"].(string)
        result.Keywords = doc["_source"].(map[string]interface{})["keywords"].(string)

        res.Results = append(res.Results, *result)
    }

    res.Message = "検索に成功しました。"

    return c.JSON(http.StatusOK, res)
}

我认为,hr_query.go 是最应该考虑的部分。
我认为,通过给搜索赋予权重,可以大幅改善用户体验的部分。

package internal

func CreateQuery(q *Query) map[string]interface{} {
  query := map[string]interface{}{}
  if q.Id != "" {
    query = map[string]interface{}{
      "query": map[string]interface{}{
        "bool": map[string]interface{}{
          "must": []map[string]interface{}{
            {
              "match": map[string]interface{}{
                "referencenumber": q.Id,
              },
            },
          },
        },
      },
    }
  } else if q.Keyword != "" && q.State != "" {
    query = map[string]interface{}{
      "query": map[string]interface{}{
        "bool": map[string]interface{}{
          "must": []map[string]interface{}{
            {
              "bool": map[string]interface{}{
                "should": []map[string]interface{}{
                  {
                    "match": map[string]interface{}{
                      "title": map[string]interface{}{
                        "query": q.Keyword,
                        "boost": 3,
                      },
                    },
                  },
                  {
                    "match": map[string]interface{}{
                      "description": map[string]interface{}{
                        "query": q.Keyword,
                        "boost": 2,
                      },
                    },
                  },
                  {
                    "match": map[string]interface{}{
                      "category": map[string]interface{}{
                        "query": q.Keyword,
                        "boost": 1,
                      },
                    },
                  },
                },
                "minimum_should_match": 1,
              },
            },
            {
              "bool": map[string]interface{}{
                "must": []map[string]interface{}{
                  {
                    "match": map[string]interface{}{
                      "state": q.State,
                    },
                  },
                },
              },
            },
          },
        },
      },
    }
  } else if q.Keyword != "" && q.State == "" {
    query = map[string]interface{}{
      "query": map[string]interface{}{
        "bool": map[string]interface{}{
          "should": []map[string]interface{}{
            {
              "match": map[string]interface{}{
                "title": map[string]interface{}{
                  "query": q.Keyword,
                  "boost": 3,
                },
              },
            },
            {
              "match": map[string]interface{}{
                "description": map[string]interface{}{
                  "query": q.Keyword,
                  "boost": 2,
                },
              },
            },
            {
              "match": map[string]interface{}{
                "category": map[string]interface{}{
                  "query": q.Keyword,
                  "boost": 1,
                },
              },
            },
          },
          "minimum_should_match": 1,
        },
      },
    }
  } else if q.Keyword == "" && q.State != "" {
    query = map[string]interface{}{
      "query": map[string]interface{}{
        "bool": map[string]interface{}{
          "must": []map[string]interface{}{
            {
              "match": map[string]interface{}{
                "state": q.State,
              },
            },
          },
        },
      },
    }
  }

  return query
}

这是连接到Elasticsearch并进行通信的部分。在这里,我想参考了在Qiita等网站上发布的文章,但是无法找到文章的URL了。。。

package internal

import (
    "os"

    Elasticsearch "github.com/elastic/go-Elasticsearch/v7"
)

func ConnectElasticsearch() (*Elasticsearch.Client, error) {
    // 環境変数 ES_ADDRESS がある場合は記述されているアドレスに接続
    // ない場合は、 http://localhost:9200 に接続
    var addr string
    if os.Getenv("ES_ADDRESS") != "" {
        addr = os.Getenv("ES_ADDRESS")
    } else {
        addr = "http://localhost:9200"
    }
    cfg := Elasticsearch.Config{
        Addresses: []string{
            addr,
        },
    }
    es, err := Elasticsearch.NewClient(cfg)

    return es, err
}

5. 我们试着在浏览器中进行确认。 .)

这样,它终于能够运转了。

如果你运行 “docker-compose up” 并执行 “go run main.go”,我想你可以从浏览器中进行确认。
我认为在 VSCode 中进行操作会更容易理解。
如果你是通过 Remote SSH 在开发环境服务器上操作的,请参考上级的说明。

你可以通过浏览器来确认这个样子。

http://localhost:5000/search?keyword=カフェ&state=東京都
{
    "message": "検索に成功しました。",
    "Results": [
        {
            "referencenumber": "test",
            "date": "2222-11-01",
            "url": "test",
            "title": "おしゃれカフェ・店舗スタッフ/ブック&カフェ/アルバイト・パート/おしゃれカフェ",
            "description": "【省略】",
            "state": "東京都",
            "city": "渋谷区",
            "country": "日本",
            "station": "山手線渋谷駅 徒歩700分",
            "jobtype": "アルバイト・パート",
            "salary": "test円",
            "category": "飲食・フード×おしゃれカフェ",
            "imageurls": "test",
            "timeshift": "週3日以上/1日3時間以上",
            "subwayaccess": "山手線渋谷駅徒歩700分",
            "keywords": "test"
        },

6. 最后

我们在マイナビ运营着许多求职网站。通过使用Go语言创建简易的求职网站,我能够重新学习技术背景。

如果你有兴趣,请一定试着创建!

bannerAds