blob: c985594c4a6df7c34c9a5fca1a2e7cce89e66cec [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package algolia
import (
"context"
"embed"
"github.com/segmentfault/pacman/log"
"reflect"
"strconv"
"strings"
"sync"
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
"github.com/apache/answer-plugins/search-algolia/i18n"
"github.com/apache/answer-plugins/util"
"github.com/apache/answer/plugin"
)
//go:embed info.yaml
var Info embed.FS
type SearchAlgolia struct {
Config *AlgoliaSearchConfig
client *search.APIClient
syncer plugin.SearchSyncer
once sync.Once
}
func init() {
uc := &SearchAlgolia{Config: &AlgoliaSearchConfig{}}
plugin.Register(uc)
}
func (s *SearchAlgolia) Info() plugin.Info {
info := &util.Info{}
info.GetInfo(Info)
return plugin.Info{
Name: plugin.MakeTranslator(i18n.InfoName),
SlugName: info.SlugName,
Description: plugin.MakeTranslator(i18n.InfoDescription),
Version: info.Version,
Author: info.Author,
Link: info.Link,
}
}
func (s *SearchAlgolia) Description() plugin.SearchDesc {
desc := plugin.SearchDesc{}
if s.Config.ShowLogo {
desc.Icon = icon
desc.Link = "https://www.algolia.com/"
}
return desc
}
func (s *SearchAlgolia) RegisterSyncer(ctx context.Context, syncer plugin.SearchSyncer) {
s.syncer = syncer
s.sync()
}
func (s *SearchAlgolia) SearchContents(ctx context.Context, cond *plugin.SearchBasicCond) (res []plugin.SearchResult, total int64, err error) {
var (
filters = "status<10"
tagFilters []string
userIDFilter string
votesFilter string
)
if len(cond.TagIDs) > 0 {
for _, tagGroup := range cond.TagIDs {
var tagsIn []string
if len(tagGroup) > 0 {
for _, tagID := range tagGroup {
tagsIn = append(tagsIn, "tags:"+tagID)
}
}
tagFilters = append(tagFilters, "("+strings.Join(tagsIn, " OR ")+")")
}
if len(tagFilters) > 0 {
filters += " AND " + strings.Join(tagFilters, " AND ")
}
}
if len(cond.UserID) > 0 {
userIDFilter = "userID:" + cond.UserID
filters += " AND " + userIDFilter
}
if cond.VoteAmount == 0 {
votesFilter = "votes=" + strconv.Itoa(cond.VoteAmount)
filters += " AND " + votesFilter
} else if cond.VoteAmount > 0 {
votesFilter = "votes>=" + strconv.Itoa(cond.VoteAmount)
filters += " AND " + votesFilter
}
var (
query = strings.TrimSpace(strings.Join(cond.Words, " "))
qres *search.SearchResponse
)
qres, err = s.client.SearchSingleIndex(
s.client.NewApiSearchSingleIndexRequest(
s.getIndexName(string(cond.Order))).
WithSearchParams(
search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetQuery(query).
SetAttributesToRetrieve([]string{"objectID", "type"}).
SetFilters(filters).
SetPage(int32(cond.Page - 1)).
SetHitsPerPage(int32(cond.PageSize)),
),
),
)
if err != nil {
return
}
if qres == nil {
return
}
for _, hit := range qres.Hits {
res = append(res, plugin.SearchResult{
ID: hit.ObjectID,
Type: hit.AdditionalProperties["type"].(string),
})
}
total = int64(*qres.NbHits)
return res, total, err
}
func (s *SearchAlgolia) SearchQuestions(ctx context.Context, cond *plugin.SearchBasicCond) (res []plugin.SearchResult, total int64, err error) {
var (
filters = "status<10 AND type:question"
tagFilters []string
userIDFilter string
viewsFilter string
answersFilter string
)
if len(cond.TagIDs) > 0 {
for _, tagGroup := range cond.TagIDs {
var tagsIn []string
if len(tagGroup) > 0 {
for _, tagID := range tagGroup {
tagsIn = append(tagsIn, "tags:"+tagID)
}
}
tagFilters = append(tagFilters, "("+strings.Join(tagsIn, " OR ")+")")
}
if len(tagFilters) > 0 {
filters += " AND " + strings.Join(tagFilters, " AND ")
}
}
if cond.QuestionAccepted == plugin.AcceptedCondFalse {
userIDFilter = "hasAccepted:false"
filters += " AND " + userIDFilter
}
if cond.ViewAmount > -1 {
viewsFilter = "views>=" + strconv.Itoa(cond.ViewAmount)
filters += " AND " + viewsFilter
}
// check answers
if cond.AnswerAmount == 0 {
answersFilter = "answers=0"
filters += " AND " + answersFilter
} else if cond.AnswerAmount > 0 {
answersFilter = "answers>=" + strconv.Itoa(cond.AnswerAmount)
filters += " AND " + answersFilter
}
var (
query = strings.TrimSpace(strings.Join(cond.Words, " "))
qres *search.SearchResponse
)
qres, err = s.client.SearchSingleIndex(
s.client.NewApiSearchSingleIndexRequest(
s.getIndexName(string(cond.Order))).
WithSearchParams(
search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetQuery(query).
SetAttributesToRetrieve([]string{"objectID", "type"}).
SetFilters(filters).
SetPage(int32(cond.Page - 1)).
SetHitsPerPage(int32(cond.PageSize)),
),
),
)
if err != nil {
return
}
if qres == nil {
return
}
for _, hit := range qres.Hits {
res = append(res, plugin.SearchResult{
ID: hit.ObjectID,
Type: hit.AdditionalProperties["type"].(string),
})
}
total = int64(*qres.NbHits)
return res, total, err
}
func (s *SearchAlgolia) SearchAnswers(ctx context.Context, cond *plugin.SearchBasicCond) (res []plugin.SearchResult, total int64, err error) {
var (
filters = "status<10 AND type:answer"
tagFilters []string
userIDFilter string
questionIDFilter string
)
if len(cond.TagIDs) > 0 {
for _, tagGroup := range cond.TagIDs {
var tagsIn []string
if len(tagGroup) > 0 {
for _, tagID := range tagGroup {
tagsIn = append(tagsIn, "tags:"+tagID)
}
}
tagFilters = append(tagFilters, "("+strings.Join(tagsIn, " OR ")+")")
}
if len(tagFilters) > 0 {
filters += " AND " + strings.Join(tagFilters, " AND ")
}
}
if cond.AnswerAccepted == plugin.AcceptedCondTrue {
userIDFilter = "hasAccepted=true"
filters += " AND " + userIDFilter
}
if len(cond.QuestionID) > 0 {
questionIDFilter = "questionID=" + cond.QuestionID
filters += questionIDFilter
}
var (
query = strings.TrimSpace(strings.Join(cond.Words, " "))
qres *search.SearchResponse
)
qres, err = s.client.SearchSingleIndex(
s.client.NewApiSearchSingleIndexRequest(
s.getIndexName(string(cond.Order))).
WithSearchParams(
search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetQuery(query).
SetAttributesToRetrieve([]string{"objectID", "type"}).
SetFilters(filters).
SetPage(int32(cond.Page - 1)).
SetHitsPerPage(int32(cond.PageSize)),
),
),
)
if err != nil {
return
}
for _, hit := range qres.Hits {
res = append(res, plugin.SearchResult{
ID: hit.ObjectID,
Type: hit.AdditionalProperties["type"].(string),
})
}
total = int64(*qres.NbHits)
return res, total, err
}
// UpdateContent updates the content to algolia server
func (s *SearchAlgolia) UpdateContent(ctx context.Context, content *plugin.SearchContent) (err error) {
record := s.parseRecordData(content)
_, err = s.client.SaveObject(s.client.NewApiSaveObjectRequest(s.getIndexName(""), record))
if err != nil {
return
}
return
}
// DeleteContent deletes the content
func (s *SearchAlgolia) DeleteContent(ctx context.Context, contentID string) (err error) {
_, err = s.client.DeleteObject(s.client.NewApiDeleteObjectRequest(s.getIndexName(""), contentID))
return
}
// connect connect to algolia server
func (s *SearchAlgolia) connect() (err error) {
s.once.Do(func() {
s.client, err = search.NewClient(s.Config.APPID, s.Config.APIKey)
if err != nil {
log.Error("algolia: connect error", err)
}
log.Info("algolia: connected")
})
return
}
func (s *SearchAlgolia) getIndexName(order string) string {
// main index
var idx = s.Config.Index
switch order {
case NewestIndex:
// the index of sort results by newest
idx = idx + "_" + NewestIndex
case ActiveIndex:
// the index of sort results by active
idx = idx + "_" + ActiveIndex
case ScoreIndex:
// the index of sort results by score
idx = idx + "_" + ScoreIndex
}
return idx
}
func (s *SearchAlgolia) parseRecordData(content *plugin.SearchContent) (contentMap map[string]any) {
// 使用反射将结构体转换为map
contentMap = make(map[string]any)
val := reflect.ValueOf(content).Elem() // 获取指针指向的实际结构体
typ := val.Type()
// each struct field
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
// fetch json tag name, if not exist use field name
fieldName := typ.Field(i).Name
jsonTag := typ.Field(i).Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "-" && parts[0] != "" {
fieldName = parts[0]
}
}
// only set the field that can be interfaced
if field.CanInterface() {
contentMap[fieldName] = field.Interface()
}
}
return
}