Используйте ElasticSearch6.0 для быстрой реализации функции полнотекстового поиска.

задняя часть база данных MySQL Elasticsearch

В этой статье не рассматриваются конкретные принципы ElasticSearch, а только описывается, как быстро импортировать данные в mysql для полнотекстового поиска.

На работе нам нужно реализовать функцию поиска и импортировать данные из существующей базы данных. Руководитель группы рекомендует использовать ElasticSearch для ее реализации. Просматривая онлайн-учебники, все они относительно старые статьи. У меня нет другого выбора, кроме как изучить самостоятельно. к документации ES я наконец-то получаю сервис.После настройки его записываю.Надеюсь, что друзья, у которых такие же потребности, смогут избежать обходных путей и быстро построить доступный сервис ElasticSearch по этому туториалу.

Строительство ЭС

Сборки ES имеют прямую загрузку zip-файлов и контейнеров докеров, Условно говоря, докер больше подходит для запуска служб ES. Удобно построить кластер или установить тестовую среду. Здесь также используется контейнерный метод, во-первых, нам нужен Dockerfile:

FROM docker.elastic.co/elasticsearch/elasticsearch-oss:6.0.0
# 提交配置 包括新的elasticsearch.yml 和 keystore.jks文件
COPY --chown=elasticsearch:elasticsearch conf/ /usr/share/elasticsearch/config/
# 安装ik
RUN ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.0.0/elasticsearch-analysis-ik-6.0.0.zip
# 安装readonlyrest
RUN ./bin/elasticsearch-plugin install https://github.com/HYY-yu/BezierCurveDemo/raw/master/readonlyrest-1.16.14_es6.0.0.zip

USER elasticsearch
CMD ./bin/elasticsearch

Вот описание вышеуказанной операции:

  1. Во-первых, вам нужно создать папку conf в каталоге того же уровня в Dockerfile, чтобы сохранить файл elasticsearch.yml (указанный позже) и keystore.jks. (jks — это самозаверяющий файл для https, пожалуйста, найдите, как его сгенерировать)
  2. ik — очень популярная библиотека сегментации китайских слов, используйте ее для поддержки китайского поиска.
  3. readonlyrest — плагин ES с открытым исходным кодом для управления пользователями и проверки безопасности.Местные тираны могут использовать пакет X-pack, поставляемый с ES, который имеет более полные функции безопасности.

эластичная конфигурация elasticsearch.yml

cluster.name: "docker-cluster"
network.host: 0.0.0.0

# minimum_master_nodes need to be explicitly set when bound on a public IP
# set to 1 to allow single node clusters
# Details: https://github.com/elastic/elasticsearch/pull/17288
discovery.zen.minimum_master_nodes: 1

# 禁止系统对ES交换内存
bootstrap.memory_lock: true

http.type: ssl_netty4

readonlyrest:
  enable: true
  ssl:
    enable: true
    keystore_file: "server.jks"
    keystore_pass: server
    key_pass: server

  access_control_rules:

    - name: "Block 1 - ROOT"
      type: allow
      groups: ["admin"]

    - name: "User read only - paper"
      groups: ["user"]
      indices: ["paper*"]
      actions: ["indices:data/read/*"]

  users:

    - username: root
      auth_key_sha256: cb7c98bae153065db931980a13bd45ee3a77cb8f27a7dfee68f686377acc33f1
      groups: ["admin"]

    - username: xiaoming
      auth_key: xiaoming:xiaoming
      groups: ["user"]

здесьbootstrap.memory_lock: trueяма,Отключить подкачку памятиВ этом документе уже объяснялось, что некоторые ОС будут перемещать временно неиспользуемую память в область жесткого диска во время выполнения, но такое поведение приведет к резкому увеличению использования ресурсов ES и даже сделает систему невосприимчивой.

В файле конфигурации уже очевидно, что пользователь root принадлежит к группе администраторов, и у администратора есть все права.Поскольку Сяомин находится в группе пользователей, он может получить доступ только к бумажному индексу, и может только читать и не может работать. Для более подробной настройки см.:документация readonlyrest

На этом подготовка к ЭП завершена.docker build -t ESImage:tagнемного,docker run -p 9200:9200 ESImage:Tagначать бежать.

Если https://127.0.0.1:9200/ возвращается

{
    "name": "VaKwrIR",
    "cluster_name": "docker-cluster",
    "cluster_uuid": "YsYdOWKvRh2swz907s2m_w",
    "version": {
        "number": "6.0.0",
        "build_hash": "8f0685b",
        "build_date": "2017-11-10T18:41:22.859Z",
        "build_snapshot": false,
        "lucene_version": "7.0.1",
        "minimum_wire_compatibility_version": "5.6.0",
        "minimum_index_compatibility_version": "5.0.0"
    },
    "tagline": "You Know, for Search"
}

Здесь находится главный герой нашего руководства, рассказывающий о нескольких часто используемых API.приставатьОтладка ES с помощью:

Замените {{url}} своим локальным адресом ES.

  • Просмотреть все плагины: {{url}}/_cat/plugins?v
  • Просмотреть все индексы: {{url}} / _ cat / index v?
  • Проверка работоспособности на ES: {{url}}/_cat/health?v
  • Просмотр текущего использования диска: {{url}}/_cat/allocation?v

Импорт данных MYSQL

Здесь я использую данные MYSQL. На самом деле, другие базы данных такие же. Ключ в том, как импортировать. Онлайн-руководство порекомендует для импорта подключаемый модуль mysql Logstash, Beat и ES. Я также экспериментировал с ним. Конфигурация сложная и документы скудные, чуть сложнее, импорт кропотливая работа, поэтому не рекомендуется. На самом деле в ES есть соответствующие библиотеки API на каждом языке, можно собирать данные в json на уровне языка и отправлять в ES через библиотеку API. Процесс примерно такой:

image

Я использую эластичную библиотеку ES от Golang.Другие языки могут идти на github для поиска сами по себе, и метод работы такой же.

Затем используйте простую базу данных, чтобы представить:

Лист бумаги

id name
1 Бумага-симулятор начальной школы № 1 в Пекине
2 Вопросы для вступительных экзаменов в пекинский общий колледж Цзянси

Таблица провинции

id name
1 Пекин
2 Цзянси

Таблица Paper_Province

paper_id province_id
1 1
2 1
2 2

Как указано выше, бумага и провинция во многих отношениях со многими ко многим. Теперь введите данные бумаги в ES, и вы можете искать по названию статьи или фильтровать провинцией. Формат данных JSON следующим образом:

{
    "id":1,
    "name": "北京第一小学模拟卷",
    "provinces":[
        {
            "id":1,
            "name":"北京"
        }
    ]
}

Сначала подготовьте файл mapping.json, который представляет собой определение структуры хранения данных в ES.

{
    "mappings":{
        "docs":{
			"include_in_all": false, 
            "properties":{
                "id":{
                    "type":"long"
                },
                "name":{
                    "type":"text",
                    "analyzer":"ik_max_word" // 使用最大词分词器
                },
                "provinces":{
                    "type":"nested",
                    "properties":{
                        "id":{
                            "type":"integer"
                        },
                        "name":{
                            "type":"text",
                            "index":"false" // 不索引
                        }
                    }
                }
            }
        }
    },
    "settings":{
        "number_of_shards":1,
        "number_of_replicas":0
    }
}

Следует отметить, что поле _all отменено.Это _all по умолчанию соберет все поля хранения и реализует безусловный поиск.Недостаток в том, что он занимает много места.

Количество шардов я поставил 1, а реплики не ставил.Ведь это не кластер,и обрабатываемых данных не много.Если есть большой объем данных для обработки,можно поставить количество осколков и реплик самостоятельно.

Сначала установите соединение с ES, ca.crt связан с самоподписью jks. Конечно, здесь я использую InsecureSkipVerify, чтобы игнорировать проверку файла сертификата.

func InitElasticSearch() {
	pool := x509.NewCertPool()
	crt, err0 := ioutil.ReadFile("conf/ca.crt")
	if err0 != nil {
		cannotOpenES(err0, "read crt file err")
		return
	}

	pool.AppendCertsFromPEM(crt)
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{RootCAs: pool, InsecureSkipVerify: true},
	}
	httpClient := &http.Client{Transport: tr}

	//后台构造elasticClient
	var err error
	elasticClient, err = elastic.NewClient(elastic.SetURL(MyConfig.ElasticUrl),
		elastic.SetErrorLog(GetLogger()),
		elastic.SetGzip(true),
		elastic.SetHttpClient(httpClient),
		elastic.SetSniff(false), // 集群嗅探,单节点记得关闭。
		elastic.SetScheme("https"),
		elastic.SetBasicAuth(MyConfig.ElasticUsername, MyConfig.ElasticPassword))
	if err != nil {
		cannotOpenES(err, "search_client_error")
		return
	}
	//elasticClient构造完成

	//查询是否有paper索引
	exist, err := elasticClient.IndexExists(MyConfig.ElasticIndexName).Do(context.Background())
	if err != nil {
		cannotOpenES(err, "exist_paper_index_check")
		return
	}

	//索引存在且通过完整性检查则不发送任何数据
	if exist {
		if !isIndexIntegrity(elasticClient) {
			//删除当前索引  准备重建
			deleteResponse, err := elasticClient.DeleteIndex(MyConfig.ElasticIndexName).Do(context.Background())
			if err != nil || !deleteResponse.Acknowledged {
				cannotOpenES(err, "delete_index_error")
				return
			}
		} else {
			return
		}
	}

	//后台查询数据库,发送数据到elasticsearch中
	go fetchDBGetAllPaperAndSendToES()
}
type PaperSearch struct {
	PaperId    int64     `gorm:"primary_key;column:F_paper_id;type:BIGINT(20)" json:"id"`
	Name       string    `gorm:"column:F_name;size:80" json:"name"`
	Provinces  []Province `gorm:"many2many:t_paper_province;" json:"provinces"`        // 试卷适用的省份
}

func fetchDBGetAllPaperAndSendToES() {
	//fetch paper
	var allPaper []PaperSearch

	GetDb().Table("t_papers").Find(&allPaper)

	//province
	for i := range allPaper {
		var allPro []Province
		GetDb().Table("t_provinces").Joins("INNER JOIN `t_paper_province` ON `t_paper_province`.`province_F_province_id` = `t_provinces`.`F_province_id`").
			Where("t_paper_province.paper_F_paper_id = ?", allPaper[i].PaperId).Find(&allPro)
		allPaper[i].Provinces = allPro
	}

	if len(allPaper) > 0 {
		//send to es - create index
		createService := GetElasticSearch().CreateIndex(MyConfig.ElasticIndexName)
		// 此处的index_default_setting就是上面mapping.json中的内容。
		createService.Body(index_default_setting)
		createResult, err := createService.Do(context.Background())
		if err != nil {
			cannotOpenES(err, "create_paper_index")
			return
		}

		if !createResult.Acknowledged || !createResult.ShardsAcknowledged {
			cannotOpenES(err, "create_paper_index_fail")
		}

		// - send all paper
		bulkRequest := GetElasticSearch().Bulk()

		for i := range allPaper {
			indexReq := elastic.NewBulkIndexRequest().OpType("create").Index(MyConfig.ElasticIndexName).Type("docs").
				Id(helper.Int64ToString(allPaper[i].PaperId)).
				Doc(allPaper[i])

			bulkRequest.Add(indexReq)
		}

		// Do sends the bulk requests to Elasticsearch
		bulkResponse, err := bulkRequest.Do(context.Background())
		if err != nil {
			cannotOpenES(err, "insert_docs_error")
			return
		}

		// Bulk request actions get cleared
		if len(bulkResponse.Created()) != len(allPaper) {
			cannotOpenES(err, "insert_docs_nums_error")
			return
		}
		//send success
	}
}

После выполнения приведенного выше кода используйте{{url}}/_cat/indices?vЧтобы увидеть, появляется ли вновь созданный индекс в ES, используйте{{url}}/papers/_searchПосмотрите, сколько документов было найдено, и если количество документов равно количеству данных, которые вы отправили в прошлом, служба поиска работает.

поиск

Теперь вы можете искать экзаменационные работы по номеру провинции и q, по умолчанию отсортированным по релевантности.

//q 搜索字符串 provinceID 限定省份id limit page 分页参数
func SearchPaper(q string, provinceId uint, limit int, page int) (list []PaperSearch, totalPage int, currentPage int, pageIsEnd int, returnErr error) {
	//不满足条件,使用数据库搜索
	if !CanUseElasticSearch && !MyConfig.UseElasticSearch {
		return SearchPaperLocal(q, courseId, gradeId, provinceId, paperTypeId, limit, page)
	}

	list = make([]PaperSimple, 0)
	totalPage = 0
	currentPage = page
	pageIsEnd = 0
	returnErr = nil

	client := GetElasticSearch()
	if client == nil {
		return SearchPaperLocal(q, courseId, gradeId, provinceId, paperTypeId, limit, page)
	}

	//ElasticSearch有问题,使用数据库搜索
	if !isIndexIntegrity(client) {
		return SearchPaperLocal(q, courseId, gradeId, provinceId, paperTypeId, limit, page)
	}

	if !client.IsRunning() {
		client.Start()
	}
	defer client.Stop()

	q = html.EscapeString(q)
	boolQuery := elastic.NewBoolQuery()
	// Paper.name
	matchQuery := elastic.NewMatchQuery("name", q)

	//省份
	if provinceId > 0 && provinceId != DEFAULT_PROVINCE_ALL {
		proBool := elastic.NewBoolQuery()
		tpro := elastic.NewTermQuery("provinces.id", provinceId)
		proNest := elastic.NewNestedQuery("provinces", proBool.Must(tpro))
		boolQuery.Must(proNest)
	}

	boolQuery.Must(matchQuery)

	for _, e := range termQuerys {
		boolQuery.Must(e)
	}

	highligt := elastic.NewHighlight()
	highligt.Field(ELASTIC_SEARCH_SEARCH_FIELD_NAME)
	highligt.PreTags(ELASTIC_SEARCH_SEARCH_FIELD_TAG_START)
	highligt.PostTags(ELASTIC_SEARCH_SEARCH_FIELD_TAG_END)
	searchResult, err2 := client.Search(MyConfig.ElasticIndexName).
		Highlight(highligt).
		Query(boolQuery).
		From((page - 1) * limit).
		Size(limit).
		Do(context.Background())

	if err2 != nil {
		// Handle error
		GetLogger().LogErr("搜索时出错 "+err2.Error(), "search_error")
		// Handle error
		returnErr = errors.New("搜索时出错")
	} else {
		if searchResult.Hits.TotalHits > 0 {
			// Iterate through results
			for _, hit := range searchResult.Hits.Hits {
				var p PaperSearch
				err := json.Unmarshal(*hit.Source, &p)
				if err != nil {
					// Deserialization failed
					GetLogger().LogErr("搜索时出错 "+err.Error(), "search_deserialization_error")
					returnErr = errors.New("搜索时出错")
					return
				}

				if len(hit.Highlight[ELASTIC_SEARCH_SEARCH_FIELD_NAME]) > 0 {
					p.Name = hit.Highlight[ELASTIC_SEARCH_SEARCH_FIELD_NAME][0]
				}

				list = append(list, p)
			}

			count := searchResult.TotalHits()

			currentPage = page
			if count > 0 {
				totalPage = int(math.Ceil(float64(count) / float64(limit)))
			}
			if currentPage >= totalPage {
				pageIsEnd = 1
			}
		} else {
			// No hits
		}
	}
	return
}

Over