golang

Обзор библиотеки bleve в Golang

  • пятница, 16 февраля 2024 г. в 00:00:13
https://habr.com/ru/companies/otus/articles/793526/

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 про асинхронную обработку данных и ее использование в высоконагруженных проектах.