Однажды в рассылке 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