Однажды в рассылке Golang Weekly мне попался проект Bleve. Это полнотекстовый поиск, который написан на Go. Проект интересный, и появилось бешеное желание получить с ним опыт работы.
Bleve может хранить данные в разных embedded БД:
- BoltDB (использует по умолчанию)
- LevelDB
- RocksDB
- Goleveldb
- forestdb
- Gtreap
Работать с Bleve просто:
import "github.com/blevesearch/bleve"
func main() {
// Откроем новый индекс
mapping := bleve.NewIndexMapping()
index, err := bleve.New("example.bleve", mapping)
// Положим не много данных
err = index.Index(identifier, your_data)
// Найдем что-нибудь
query := bleve.NewMatchQuery("text")
search := bleve.NewSearchRequest(query)
searchResults, err := index.Search(search)
}
Все просто и понятно. Но выглядит оно не с реального мира. Чтобы быть ближе к реальному миру, сделаем бота для Slack, который будет хранить историю.
Архитектура бота
Сервис для работы со slack;
Сервис индекс. Для хранения и поиска сообщений.
План
- Берем https://api.slack.com/methods/channels.history для работы со слаком;
- Берем Bleve для поиска и хранения истории;
- Если к нам пришло сообщение не по меншну бота — кладем в индекс;
- Если пришло сообщение с меншном — чистим и ищем по текущему каналу;
Slack
Со слаком все просто и пример по сути будет чуть сложнее, чем пример из репо
Единственное, что нам потребуется — два метода, чтобы проверить, адресовано ли боту сообщение и очистить его от имени бота
import "strings"
func isToMe(message string) bool {
return strings.Contains(message, fmt.Sprintf("<@%s>", ss.me))
}
func cleanMessage(message string) string {
return strings.Replace(message, fmt.Sprintf("<@%s> ", ss.me), "", -1)
}
Bleve
Учитывая то, что я люблю использовать goleveldb как встраиваемую БД для своих проектов. В этом проекте решил использовать ее же.
Хранить в Bleve будем данные посложнее в виде:
type IndexData struct {
ID string `json:"id"`
Username string `json:"username"`
Message string `json:"message"`
Channel string `json:"channel"`
Timestamp string `json:"timestamp"`
}
Создадим индекс с Goleveldb в качестве БД:
import (
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/index/store/goleveldb"
)
func createIndex() (bleve.Index, error) {
indexName := "history.bleve"
index, err := bleve.Open(indexName)
if err == bleve.ErrorIndexPathDoesNotExist {
mapping := buildMapping()
kvStore := goleveldb.Name
kvConfig := map[string]interface{}{
"create_if_missing": true,
}
index, err = bleve.NewUsing(indexName, mapping, "upside_down", kvStore, kvConfig)
}
if err != nil {
return err
}
}
и метод buildMapping, который создаст нам mapping для хранения:
func (ss *SearchService) buildMapping() *bleve.IndexMapping {
ruFieldMapping := bleve.NewTextFieldMapping()
ruFieldMapping.Analyzer = ru.AnalyzerName
eventMapping := bleve.NewDocumentMapping()
eventMapping.AddFieldMappingsAt("message", ruFieldMapping)
mapping := bleve.NewIndexMapping()
mapping.DefaultMapping = eventMapping
mapping.DefaultAnalyzer = ru.AnalyzerName
return mapping
}
С поиском все чуть сложнее:
func (ss *SearchService) Search(query, channel string) (*bleve.SearchResult, error) {
stringQuery := fmt.Sprintf("/.*%s.*/", query)
// NewTermQuery создает Query для нахождения значений в индексе, которые строго совпадают с запросом
ch := bleve.NewTermQuery(channel)
// Создаем Query для совпадений фраз в индексе. Анализатор выбирается по полю. Ввод анализируется этим анализатором. Токенезированные выражения от анализа используются для посторения поисковой фразы. Результирующие документы должны совпадать с этой фразой.
mq := bleve.NewMatchPhraseQuery(query)
// Создаем Query для поиска значений в индексе по регулярному выражению
rq := bleve.NewRegexpQuery(query)
// Создаем Query для поиска документов, результаты которого удовлетворят поисковой строке.
qsq := bleve.NewQueryStringQuery(stringQuery)
// Создаем составную Query Результат должен удовлетворять хотя бы одной Query.
q := bleve.NewDisjunctionQuery([]bleve.Query{ch, mq, rq, qsq})
search := bleve.NewSearchRequest(q)
search.Fields = []string{"username", "message", "channel", "timestamp"}
return ss.index.Search(search)
}
Соединив все вместе, мы получим бота, который сохраняет историю и может искать по ней без тяжеловесной жавы на примерах ElasticSearch, Solr.
Полный код проекта доступен на Github
Автор: Gen1us2k