Обзор библиотеки bleve в Golang
Bleve предоставляет индексации любых структур данных Go, он поддерживает различные типы полей: текст, числа, даты и логические значения, а также разнообразие запросов: от простых терминов до фраз и сложных булевых запросов.
Чтобы начать работу с Bleve, нужно установить саму библиотеку в рабочее пространство Go. Процесс установки выполняется с помощью команды go get
:
go get -u github.com/blevesearch/bleve
Bleve
Создание индекса начинается с определения маппинга. Маппинг — это описание структуры данных, которое указывает Bleve, как индексировать и хранить различные поля документов:
import "github.com/blevesearch/bleve"
func createIndex() {
// определение базового маппинга
mapping := bleve.NewIndexMapping()
// создание индекса с базовым маппингом
index, err := bleve.New("example.bleve", mapping)
if err != nil {
panic(err)
}
}
После создания индекса можно добавить в него данные. Это делается с помощью метода Index
, принимающего идентификатор документа и сам документ:
type BlogPost struct {
Title string
Content string
Tags []string
Author string
}
func indexData(index bleve.Index) {
post := BlogPost{
Title: "Bleve: индексация в Go",
Content: "Bleve предоставляет разработчикам...",
Tags: []string{"поиск", "индексация", "go"},
Author: "Кот",
}
err := index.Index("post_1", post)
if err != nil {
log.Fatal(err)
}
}
Для выполнения поиска в индексе Bleve используется метод Search
. Пример поиска документов по тексту:
func searchIndex(index bleve.Index) {
query := bleve.NewMatchQuery("индексация")
search := bleve.NewSearchRequest(query)
searchResults, err := index.Search(search)
if err != nil {
log.Fatal(err)
}
log.Printf("Найдено документов: %d", searchResults.Total)
}
Можно настроить маппинг для текстового поля с использованием конкретного анализатора для обработки текста на английском языке:
textFieldMapping := bleve.NewTextFieldMapping() // текстовый тип поля
textFieldMapping.Analyzer = "en"
docMapping := bleve.NewDocumentMapping()
docMapping.AddFieldMappingsAt("description", textFieldMapping)
mapping := bleve.NewIndexMapping()
mapping.AddDocumentMapping("myDoc", docMapping)
index, err := bleve.New("path/to/index.bleve", mapping)
if err != nil {
log.Fatal(err)
}
Создаём маппинг для документа с типом »myDoc», в котором есть текстовое поле »description», обрабатываемое английским анализатором
В первой строчке здесь юзаем текстовый тип поля. Можно объявить числовое поле с помощью bleve.NewNumericFieldMapping()
или к примеру поле даты bleve.NewDateTimeFieldMapping()
.
Динамические маппинги позволяют автоматически определять типы полей и применять к ним соответствующие настройки маппинга:
dynamicMapping := bleve.NewDocumentMapping()
// настройка динамического маппинга для обработки всех текстовых полей
dynamicMapping.DefaultTextFieldMapping = bleve.NewTextFieldMapping()
dynamicMapping.DefaultTextFieldMapping.Analyzer = "en"
mapping := bleve.NewIndexMapping()
mapping.DefaultMapping = dynamicMapping
index, err := bleve.New("example.bleve", mapping)
if err != nil {
panic(err)
}
Запросы
TermQuery
ищет документы, содержащие точное значение термина в указанном поле. Это самый базовый тип запроса:
query := bleve.NewTermQuery("specific term")
query.SetField("fieldName")
searchRequest := bleve.NewSearchRequest(query)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents matching 'specific term'\n", searchResult.Total)
MatchQuery
предназначен для поиска документов, содержащих термины, соответствующие запросу, с учётом анализатора поля, звучит намного круче чем termquery:
query := bleve.NewMatchQuery("text to match")
query.SetField("content")
searchRequest := bleve.NewSearchRequest(query)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents matching 'text to match'\n", searchResult.Total)
PrefixQuery
ищет документы, в которых поля содержат термины, начинающиеся с указанного префикса:
query := bleve.NewPrefixQuery("pre")
query.SetField("fieldName")
searchRequest := bleve.NewSearchRequest(query)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents with terms starting with 'pre'\n", searchResult.Total)
FuzzyQuery
поддерживает поиск по схожим терминам, опираясь на заданное расстояние Левенштейна.
query := bleve.NewFuzzyQuery("fuzze term")
query.SetFuzziness(2)
query.SetField("fieldName")
searchRequest := bleve.NewSearchRequest(query)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents with terms similar to 'fuzze term'\n", searchResult.Total)
BoolQuery
позволяет комбинировать другие запросы с использованием логических операторов:
query1 := bleve.NewMatchQuery("first term")
query2 := bleve.NewMatchQuery("second term")
boolQuery := bleve.NewBooleanQuery()
boolQuery.AddMust(query1)
boolQuery.AddShould(query2)
searchRequest := bleve.NewSearchRequest(boolQuery)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents matching conditions\n", searchResult.Total)
PhraseQuery
используется для поиска точной последовательности слов в документе:
phraseQuery := bleve.NewPhraseQuery([]string{"search", "phrase"}, "fieldName")
searchRequest := bleve.NewSearchRequest(phraseQuery)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents matching the exact phrase\n", searchResult.Total)
DateRangeQuery
позволяет искать документы с датами в определённом диапазоне. Этот тип запроса оч хорошо подходит для фильтрации событий, записей или любых данных, связанных с временными интервалами:
startDate := "2000-01-01"
endDate := "2024-12-31"
dateRangeQuery := bleve.NewDateRangeQuery(startDate, endDate)
dateRangeQuery.SetField("dateField")
searchRequest := bleve.NewSearchRequest(dateRangeQuery)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents within the date range\n", searchResult.Total)
GeoDistanceQuery
предназначен для поиска документов, находящихся в определённом радиусе от заданной географической точки:
location := bleve.NewGeoPoint(37.7749, -122.4194) // Пример для Сан-Франциско
geoQuery := bleve.NewGeoDistanceQuery(location.Lat, location.Lon, "100km")
geoQuery.SetField("locationField")
searchRequest := bleve.NewSearchRequest(geoQuery)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents within 100km of the location\n", searchResult.Total)
WildcardQuery
поддерживает поиск с использованием подстановочных знаков, что позволяет находить документы с частичным совпадением:
wildcardQuery := bleve.NewWildcardQuery("te*t")
wildcardQuery.SetField("fieldName")
searchRequest := bleve.NewSearchRequest(wildcardQuery)
searchResult, err := index.Search(searchRequest)
if err != nil {
log.Fatalf("error executing search: %v", err)
}
fmt.Printf("Found %d documents matching the wildcard pattern\n", searchResult.Total)
Анализаторы
Анализаторы в Bleve — это комбинации токенайзеров и фильтров (токенов и символов), предназначенные для обработки текста определенным образом. Bleve предлагает несколько встроенных анализаторов, например, en
для английского языка, которые настраиваются для выполнения стемминга, удаления стоп-слов и т.д
Пример использования встроенного анализатора:
mapping := bleve.NewIndexMapping()
textFieldMapping := bleve.NewTextFieldMapping()
textFieldMapping.Analyzer = "en"
mapping.DefaultMapping.AddFieldMappingsAt("description", textFieldMapping)
Токенайзеры разбивают текст на токены (обычно слова или фразы), которые затем индексируются. В Bleve доступны различные встроенные токенайзеры, к примеруunicode
, который разбивает текст по пробелам и пунктуации:
customAnalyzer := bleve.NewAnalyzer()
customAnalyzer.SetTokenizer(bleve.NewUnicodeTokenizer())
Фильтры токенов применяются к каждому токену отдельно, позволяя модифицировать, добавлять или удалять токены. Например, фильтр ToLower
преобразует все токены к нижнему регистру:
customAnalyzer := bleve.NewAnalyzer()
customAnalyzer.SetTokenizer(bleve.NewUnicodeTokenizer())
customAnalyzer.AddTokenFilter(bleve.NewLowerCaseTokenFilter())
Можно создать кастомный анализатор:
indexMapping := bleve.NewIndexMapping()
customAnalyzer := bleve.NewAnalyzerNamed("custom_analyzer")
customAnalyzer.SetTokenizer(bleve.NewUnicodeTokenizer())
customAnalyzer.AddTokenFilter(bleve.NewLowerCaseTokenFilter())
customAnalyzer.AddTokenFilter(bleve.NewStopTokenFilter())
indexMapping.AddCustomAnalyzer("custom_analyzer", customAnalyzer)
textFieldMapping := bleve.NewTextFieldMapping()
textFieldMapping.Analyzer = "custom_analyzer"
indexMapping.DefaultMapping.AddFieldMappingsAt("custom_text", textFieldMapping)
Также есть символьный фильтр. Он применяется к тексту перед его разбиением на токены. Эти фильтры работают на уровне отдельных символов, позволяя изменять, удалять или добавлять символы в исходный текст. Символьные фильтры могут быть использованы для предварительной обработки текста, например, для удаления специальных символов, приведения текста к нижнему регистру или замены символов.
В Bleve напрямую символьные фильтры не выделяются как отдельная категория для настройки анализаторов, в отличие от токенайзеров и фильтров токенов. Но все эт можно реализовать на уровне кастомных анализаторов или в процессе предварительной обработки данных перед индексацией.
Предположим, мы хотим создать символьный фильтр, который удаляет все знаки препинания из текста перед токенизацией. Это можно сделать, определив функцию предварительной обработки текста:
import (
"github.com/blevesearch/bleve"
"strings"
"unicode"
)
// функция для удаления знаков препинания из строки
func removePunctuation(text string) string {
return strings.Map(func(r rune) rune {
if unicode.IsPunct(r) {
return -1 // Удаление символа
}
return r
}, text)
}
// пример использования в кастомном анализаторе
func createCustomAnalyzerWithCharFilter(indexMapping *bleve.IndexMapping) {
textMapping := bleve.NewTextFieldMapping()
// допустм что customAnalyzer уже определён
customAnalyzer := &bleve.CustomAnalyzer{
Tokenizer: bleve.NewUnicodeTokenizer(),
TokenFilters: []string{"lower_case", "stop_en"},
CharFilters: []bleve.CharFilter{
// используем функцию для обработки текста здесь
removePunctuation,
},
}
indexMapping.AddCustomAnalyzer("custom_with_char_filter", customAnalyzer)
textMapping.Analyzer = "custom_with_char_filter"
indexMapping.DefaultMapping.AddFieldMappingsAt("myTextField", textMapping)
}
Bleve позволяет строить высокопроизводительные и функционально богатые приложения.
В завершение хочу порекомендовать вам бесплатный вебинар курса Highload Architect про асинхронную обработку данных и ее использование в высоконагруженных проектах.