// Copyright 2022 ROC. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package search import ( "strings" "github.com/Masterminds/semver/v3" "github.com/rocboss/paopao-ce/internal/core" "github.com/rocboss/paopao-ce/pkg/json" "github.com/rocboss/paopao-ce/pkg/zinc" "github.com/sirupsen/logrus" ) var ( _ core.TweetSearchService = (*zincTweetSearchServant)(nil) _ core.VersionInfo = (*zincTweetSearchServant)(nil) ) type zincTweetSearchServant struct { tweetSearchFilter indexName string client *zinc.ZincClient publicFilter string privateFilter string friendFilter string } func (s *zincTweetSearchServant) Name() string { return "Zinc" } func (s *zincTweetSearchServant) Version() *semver.Version { return semver.MustParse("v0.2.0") } func (s *zincTweetSearchServant) IndexName() string { return s.indexName } func (s *zincTweetSearchServant) AddDocuments(data []core.TsDocItem, primaryKey ...string) (bool, error) { if len(data) == 0 { return true, nil } buf := make([]map[string]any, 0, len(data)+1) if len(primaryKey) > 0 { buf = append(buf, map[string]any{ "index": map[string]any{ "_index": s.indexName, "_id": primaryKey[0], }, }) } else { buf = append(buf, map[string]any{ "index": map[string]any{ "_index": s.indexName, }, }) } docs := s.toDocs(data) buf = append(buf, docs...) return s.client.BulkPushDoc(buf) } func (s *zincTweetSearchServant) DeleteDocuments(identifiers []string) error { for _, id := range identifiers { if err := s.client.DelDoc(s.indexName, id); err != nil { return err } } return nil } func (s *zincTweetSearchServant) Search(user *core.User, q *core.QueryReq, offset, limit int) (resp *core.QueryResp, err error) { if q.Type == core.SearchTypeDefault && q.Query != "" { resp, err = s.queryByContent(user, q, offset, limit) } else if q.Type == core.SearchTypeTag && q.Query != "" { resp, err = s.queryByTag(user, q, offset, limit) } else { resp, err = s.queryAny(user, offset, limit) } if err != nil { logrus.Errorf("zincTweetSearchServant.search searchType:%s query:%s error:%v", q.Type, q.Query, err) return } logrus.Debugf("zincTweetSearchServant.Search type:%s query:%s resp Hits:%d NbHits:%d offset: %d limit:%d ", q.Type, q.Query, len(resp.Items), resp.Total, offset, limit) s.filterResp(user, resp) return } func (s *zincTweetSearchServant) queryByContent(user *core.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { resp, err := s.client.EsQuery(s.indexName, map[string]any{ "query": map[string]any{ "match_phrase": map[string]any{ "content": q.Query, }, }, "sort": []string{"-is_top", "-latest_replied_on"}, "from": offset, "size": limit, }) if err != nil { return nil, err } return s.postsFrom(resp) } func (s *zincTweetSearchServant) queryByTag(user *core.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { resp, err := s.client.ApiQuery(s.indexName, map[string]any{ "search_type": "querystring", "query": map[string]any{ "term": "tags." + q.Query + ":1", }, "sort_fields": []string{"-is_top", "-latest_replied_on"}, "from": offset, "max_results": limit, }) if err != nil { return nil, err } return s.postsFrom(resp) } func (s *zincTweetSearchServant) queryAny(user *core.User, offset, limit int) (*core.QueryResp, error) { queryMap := map[string]any{ "query": map[string]any{ "match_all": map[string]string{}, }, "sort": []string{"-is_top", "-latest_replied_on"}, "from": offset, "size": limit, } resp, err := s.client.EsQuery(s.indexName, queryMap) if err != nil { return nil, err } return s.postsFrom(resp) } func (s *zincTweetSearchServant) postsFrom(resp *zinc.QueryResultT) (*core.QueryResp, error) { posts := make([]*core.PostFormated, 0, len(resp.Hits.Hits)) for _, hit := range resp.Hits.Hits { item := &core.PostFormated{} raw, err := json.Marshal(hit.Source) if err != nil { return nil, err } if err = json.Unmarshal(raw, item); err != nil { return nil, err } posts = append(posts, item) } return &core.QueryResp{ Items: posts, Total: resp.Hits.Total.Value, }, nil } func (s *zincTweetSearchServant) createIndex() { // 不存在则创建索引 s.client.CreateIndex(s.indexName, &zinc.ZincIndexProperty{ "id": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Store: true, Sortable: true, }, "user_id": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Store: true, }, "comment_count": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "collection_count": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "upvote_count": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "visibility": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "is_top": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "is_essence": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "content": &zinc.ZincIndexPropertyT{ Type: "text", Index: true, Store: true, Aggregatable: true, Highlightable: true, Analyzer: "gse_search", SearchAnalyzer: "gse_standard", }, "tags": &zinc.ZincIndexPropertyT{ Type: "keyword", Index: true, Store: true, }, "ip_loc": &zinc.ZincIndexPropertyT{ Type: "keyword", Index: true, Store: true, }, "latest_replied_on": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "attachment_price": &zinc.ZincIndexPropertyT{ Type: "numeric", Sortable: true, Store: true, }, "created_on": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, "modified_on": &zinc.ZincIndexPropertyT{ Type: "numeric", Index: true, Sortable: true, Store: true, }, }) } func (s *zincTweetSearchServant) toDocs(data []core.TsDocItem) []map[string]any { docs := make([]map[string]any, 0, len(data)) for _, d := range data { tagMaps := map[string]int8{} for _, tag := range strings.Split(d.Post.Tags, ",") { tagMaps[tag] = 1 } docs = append(docs, map[string]any{ "id": d.Post.ID, "user_id": d.Post.UserID, "comment_count": d.Post.CommentCount, "collection_count": d.Post.CollectionCount, "upvote_count": d.Post.UpvoteCount, "visibility": d.Post.Visibility, "is_top": d.Post.IsTop, "is_essence": d.Post.IsEssence, "content": d.Content, "tags": tagMaps, "ip_loc": d.Post.IPLoc, "latest_replied_on": d.Post.LatestRepliedOn, "attachment_price": d.Post.AttachmentPrice, "created_on": d.Post.CreatedOn, "modified_on": d.Post.ModifiedOn, }) } return docs }