Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
783 lines
24 KiB
Go
783 lines
24 KiB
Go
package tencentvectordb
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"github.com/tencent/vectordatabase-sdk-go/tcvdbtext/encoder"
|
|
"github.com/tencent/vectordatabase-sdk-go/tcvectordb"
|
|
)
|
|
|
|
const copyIndicesQueryPageSize int64 = 500
|
|
|
|
// NewTencentVectorDBRetrieveEngineRepository creates a Tencent VectorDB-backed retrieve repository.
|
|
func NewTencentVectorDBRetrieveEngineRepository(
|
|
client *tcvectordb.RpcClient,
|
|
databaseName string,
|
|
indexCfg *types.IndexConfig,
|
|
) interfaces.RetrieveEngineRepository {
|
|
if databaseName == "" {
|
|
databaseName = os.Getenv(envTencentVectorDBDatabase)
|
|
}
|
|
if databaseName == "" {
|
|
databaseName = defaultDatabaseName
|
|
}
|
|
|
|
collectionBaseName := types.ResolveCollectionName(indexCfg, envTencentVectorDBCollection, defaultCollectionName)
|
|
return &repository{
|
|
client: client,
|
|
databaseName: databaseName,
|
|
collectionBaseName: collectionBaseName,
|
|
useDimensionSuffix: shouldUseDimensionSuffix(indexCfg),
|
|
shardsNum: defaultIfZero(indexCfg.GetShardsNum(1), 1),
|
|
replicasNum: resolveReplicaNumber(indexCfg),
|
|
}
|
|
}
|
|
|
|
func resolveReplicaNumber(indexCfg *types.IndexConfig) int {
|
|
if indexCfg != nil && indexCfg.ReplicaNumber > 0 {
|
|
return indexCfg.ReplicaNumber
|
|
}
|
|
if raw := strings.TrimSpace(os.Getenv(envTencentVectorDBReplicaNum)); raw != "" {
|
|
replicas, err := strconv.Atoi(raw)
|
|
if err == nil && replicas >= 0 {
|
|
return replicas
|
|
}
|
|
}
|
|
return defaultReplicaNumber
|
|
}
|
|
|
|
func (r *repository) EngineType() types.RetrieverEngineType {
|
|
return types.TencentVectorDBRetrieverEngineType
|
|
}
|
|
|
|
func (r *repository) Support() []types.RetrieverType {
|
|
return []types.RetrieverType{types.KeywordsRetrieverType, types.VectorRetrieverType}
|
|
}
|
|
|
|
func (r *repository) Save(ctx context.Context, indexInfo *types.IndexInfo, params map[string]any) error {
|
|
return r.BatchSave(ctx, []*types.IndexInfo{indexInfo}, params)
|
|
}
|
|
|
|
func (r *repository) BatchSave(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) error {
|
|
log := logger.GetLogger(ctx)
|
|
if len(indexInfoList) == 0 {
|
|
return nil
|
|
}
|
|
|
|
embeddingsByDimension := make(map[int][]*vectorEmbedding)
|
|
for _, indexInfo := range indexInfoList {
|
|
embedding := toVectorEmbedding(indexInfo, params)
|
|
if len(embedding.Embedding) == 0 {
|
|
log.Warnf("[TencentVectorDB] skip empty embedding for chunk_id=%s", indexInfo.ChunkID)
|
|
continue
|
|
}
|
|
dim := len(embedding.Embedding)
|
|
embeddingsByDimension[dim] = append(embeddingsByDimension[dim], embedding)
|
|
}
|
|
if len(embeddingsByDimension) == 0 {
|
|
return nil
|
|
}
|
|
|
|
bm25, err := r.bm25Encoder()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buildIndex := true
|
|
for dim, embeddings := range embeddingsByDimension {
|
|
docs, err := r.toDocumentsWithSparseVectors(bm25, embeddings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := r.ensureCollection(ctx, dim); err != nil {
|
|
return err
|
|
}
|
|
collectionName := r.collectionName(dim)
|
|
_, err = r.client.Database(r.databaseName).Collection(collectionName).Upsert(
|
|
ctx,
|
|
docs,
|
|
&tcvectordb.UpsertDocumentParams{BuildIndex: &buildIndex},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb batch save %s: %w", collectionName, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) EstimateStorageSize(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) int64 {
|
|
var total int64
|
|
for _, indexInfo := range indexInfoList {
|
|
embedding := toVectorEmbedding(indexInfo, params)
|
|
total += int64(len(embedding.Content))
|
|
total += int64(len(embedding.Embedding) * 4)
|
|
total += int64(len(embedding.Content) * 2)
|
|
total += int64(len(embedding.SourceID) + len(embedding.ChunkID) + len(embedding.KnowledgeID) + len(embedding.KnowledgeBaseID) + 256)
|
|
}
|
|
logger.GetLogger(ctx).Infof("[TencentVectorDB] estimated storage size for %d indices: %d bytes", len(indexInfoList), total)
|
|
return total
|
|
}
|
|
|
|
func (r *repository) DeleteByChunkIDList(ctx context.Context, chunkIDList []string, dimension int, knowledgeType string) error {
|
|
return r.deleteByFilter(ctx, dimension, tcvectordb.In(fieldChunkID, chunkIDList))
|
|
}
|
|
|
|
func (r *repository) DeleteBySourceIDList(ctx context.Context, sourceIDList []string, dimension int, knowledgeType string) error {
|
|
return r.deleteByFilter(ctx, dimension, tcvectordb.In(fieldSourceID, sourceIDList))
|
|
}
|
|
|
|
func (r *repository) DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int, knowledgeType string) error {
|
|
return r.deleteByFilter(ctx, dimension, tcvectordb.In(fieldKnowledgeID, knowledgeIDList))
|
|
}
|
|
|
|
func (r *repository) CopyIndices(
|
|
ctx context.Context,
|
|
sourceKnowledgeBaseID string,
|
|
sourceToTargetKBIDMap map[string]string,
|
|
sourceToTargetChunkIDMap map[string]string,
|
|
targetKnowledgeBaseID string,
|
|
dimension int,
|
|
knowledgeType string,
|
|
) error {
|
|
if len(sourceToTargetChunkIDMap) == 0 {
|
|
return nil
|
|
}
|
|
collectionName := r.collectionName(dimension)
|
|
ids := slices.Collect(maps.Keys(sourceToTargetChunkIDMap))
|
|
|
|
var embeddings []*vectorEmbedding
|
|
for offset := int64(0); ; offset += copyIndicesQueryPageSize {
|
|
query, err := r.client.Database(r.databaseName).Collection(collectionName).Query(
|
|
ctx,
|
|
nil,
|
|
copySourceQueryParams(sourceKnowledgeBaseID, ids, offset),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb query source indices: %w", err)
|
|
}
|
|
embeddings = append(embeddings, remapCopiedEmbeddings(
|
|
query.Documents,
|
|
sourceToTargetKBIDMap,
|
|
sourceToTargetChunkIDMap,
|
|
targetKnowledgeBaseID,
|
|
)...)
|
|
if len(query.Documents) < int(copyIndicesQueryPageSize) {
|
|
break
|
|
}
|
|
}
|
|
if len(embeddings) == 0 {
|
|
return nil
|
|
}
|
|
|
|
bm25, err := r.bm25Encoder()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
docs, err := r.toDocumentsWithSparseVectors(bm25, embeddings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buildIndex := true
|
|
_, err = r.client.Database(r.databaseName).Collection(collectionName).Upsert(
|
|
ctx,
|
|
docs,
|
|
&tcvectordb.UpsertDocumentParams{BuildIndex: &buildIndex},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb copy indices: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) BatchUpdateChunkEnabledStatus(ctx context.Context, chunkStatusMap map[string]bool) error {
|
|
if len(chunkStatusMap) == 0 {
|
|
return nil
|
|
}
|
|
grouped := make(map[bool][]string)
|
|
for chunkID, enabled := range chunkStatusMap {
|
|
grouped[enabled] = append(grouped[enabled], chunkID)
|
|
}
|
|
for enabled, chunkIDs := range grouped {
|
|
if err := r.updateChunkFields(ctx, chunkIDs, map[string]tcvectordb.Field{fieldIsEnabled: {Val: boolToUint64(enabled)}}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) BatchUpdateChunkTagID(ctx context.Context, chunkTagMap map[string]string) error {
|
|
if len(chunkTagMap) == 0 {
|
|
return nil
|
|
}
|
|
grouped := make(map[string][]string)
|
|
for chunkID, tagID := range chunkTagMap {
|
|
grouped[tagID] = append(grouped[tagID], chunkID)
|
|
}
|
|
for tagID, chunkIDs := range grouped {
|
|
if err := r.updateChunkFields(ctx, chunkIDs, map[string]tcvectordb.Field{fieldTagID: {Val: tagID}}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) Retrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error) {
|
|
switch params.RetrieverType {
|
|
case types.VectorRetrieverType:
|
|
return r.VectorRetrieve(ctx, params)
|
|
case types.KeywordsRetrieverType:
|
|
return r.KeywordsRetrieve(ctx, params)
|
|
default:
|
|
return nil, fmt.Errorf("invalid retriever type: %s", params.RetrieverType)
|
|
}
|
|
}
|
|
|
|
func (r *repository) VectorRetrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error) {
|
|
dimension := len(params.Embedding)
|
|
if dimension == 0 {
|
|
return r.retrieveResult(nil, types.VectorRetrieverType), nil
|
|
}
|
|
|
|
collectionName := r.collectionName(dimension)
|
|
exists, err := r.client.Database(r.databaseName).ExistsCollection(ctx, collectionName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tencent vectordb check collection %s: %w", collectionName, err)
|
|
}
|
|
if !exists {
|
|
return r.retrieveResult(nil, types.VectorRetrieverType), nil
|
|
}
|
|
|
|
limit := int64(params.TopK)
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
searchParams := &tcvectordb.SearchDocumentParams{
|
|
Filter: r.baseFilter(params),
|
|
Params: &tcvectordb.SearchDocParams{Ef: 100},
|
|
RetrieveVector: false,
|
|
OutputFields: outputFields(),
|
|
Limit: limit,
|
|
}
|
|
if params.Threshold > 0 {
|
|
radius := float32(params.Threshold)
|
|
searchParams.Radius = &radius
|
|
}
|
|
|
|
search, err := r.client.Database(r.databaseName).Collection(collectionName).Search(ctx, [][]float32{params.Embedding}, searchParams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tencent vectordb vector search %s: %w", collectionName, err)
|
|
}
|
|
if len(search.Documents) == 0 {
|
|
return r.retrieveResult(nil, types.VectorRetrieverType), nil
|
|
}
|
|
|
|
results := make([]*types.IndexWithScore, 0, len(search.Documents[0]))
|
|
for _, doc := range search.Documents[0] {
|
|
embedding := fromDocument(doc)
|
|
results = append(results, toIndexWithScore(embedding, types.MatchTypeEmbedding))
|
|
}
|
|
return r.retrieveResult(results, types.VectorRetrieverType), nil
|
|
}
|
|
|
|
func (r *repository) KeywordsRetrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error) {
|
|
log := logger.GetLogger(ctx)
|
|
query := strings.TrimSpace(params.Query)
|
|
if query == "" {
|
|
return r.retrieveResult(nil, types.KeywordsRetrieverType), nil
|
|
}
|
|
|
|
bm25, err := r.bm25Encoder()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
queryVectors, err := bm25.EncodeQueries([]string{query})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tencent vectordb encode keyword query: %w", err)
|
|
}
|
|
if len(queryVectors) == 0 || len(queryVectors[0]) == 0 {
|
|
return r.retrieveResult(nil, types.KeywordsRetrieverType), nil
|
|
}
|
|
|
|
collections, err := r.client.Database(r.databaseName).ListCollection(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tencent vectordb list collections: %w", err)
|
|
}
|
|
|
|
limit := params.TopK
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
results := make([]*types.IndexWithScore, 0, limit)
|
|
matchedCollections := 0
|
|
failedCollections := 0
|
|
for _, collection := range collections.Collections {
|
|
if !r.matchesCollection(collection.CollectionName) {
|
|
continue
|
|
}
|
|
matchedCollections++
|
|
|
|
search, err := r.client.Database(r.databaseName).Collection(collection.CollectionName).FullTextSearch(
|
|
ctx,
|
|
tcvectordb.FullTextSearchParams{
|
|
Filter: r.baseFilter(params),
|
|
RetrieveVector: false,
|
|
OutputFields: outputFields(),
|
|
Limit: &limit,
|
|
Match: &tcvectordb.FullTextSearchMatchOption{
|
|
FieldName: fieldSparseVector,
|
|
Data: queryVectors,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
failedCollections++
|
|
log.Warnf("[TencentVectorDB] keyword search failed in %s: %v", collection.CollectionName, err)
|
|
continue
|
|
}
|
|
if len(search.Documents) == 0 {
|
|
continue
|
|
}
|
|
for _, doc := range search.Documents[0] {
|
|
embedding := fromDocument(doc)
|
|
results = append(results, toIndexWithScore(embedding, types.MatchTypeKeywords))
|
|
}
|
|
}
|
|
if matchedCollections > 0 && failedCollections == matchedCollections {
|
|
return nil, fmt.Errorf("tencent vectordb keyword search failed in all matched collections; ensure collections have the %q sparse vector index and reimport data if they were created before keyword support", fieldSparseVector)
|
|
}
|
|
|
|
sort.SliceStable(results, func(i, j int) bool {
|
|
return results[i].Score > results[j].Score
|
|
})
|
|
if len(results) > limit {
|
|
results = results[:limit]
|
|
}
|
|
return r.retrieveResult(results, types.KeywordsRetrieverType), nil
|
|
}
|
|
|
|
func (r *repository) ensureCollection(ctx context.Context, dimension int) error {
|
|
if _, ok := r.initialized.Load(dimension); ok {
|
|
return nil
|
|
}
|
|
|
|
if _, err := r.client.CreateDatabaseIfNotExists(ctx, r.databaseName); err != nil {
|
|
return fmt.Errorf("tencent vectordb ensure database %s: %w", r.databaseName, err)
|
|
}
|
|
|
|
collectionName := r.collectionName(dimension)
|
|
exists, err := r.client.Database(r.databaseName).ExistsCollection(ctx, collectionName)
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb check collection %s: %w", collectionName, err)
|
|
}
|
|
if exists {
|
|
r.initialized.Store(dimension, true)
|
|
return nil
|
|
}
|
|
|
|
indexes := tcvectordb.Indexes{
|
|
VectorIndex: []tcvectordb.VectorIndex{
|
|
{
|
|
FilterIndex: tcvectordb.FilterIndex{
|
|
FieldName: fieldVector,
|
|
FieldType: tcvectordb.Vector,
|
|
IndexType: tcvectordb.HNSW,
|
|
},
|
|
Dimension: uint32(dimension),
|
|
MetricType: tcvectordb.COSINE,
|
|
Params: &tcvectordb.HNSWParam{
|
|
M: 16,
|
|
EfConstruction: 200,
|
|
},
|
|
},
|
|
},
|
|
SparseVectorIndex: []tcvectordb.SparseVectorIndex{
|
|
{
|
|
FieldName: fieldSparseVector,
|
|
FieldType: tcvectordb.SparseVector,
|
|
IndexType: tcvectordb.SPARSE_INVERTED,
|
|
MetricType: tcvectordb.IP,
|
|
},
|
|
},
|
|
FilterIndex: []tcvectordb.FilterIndex{
|
|
{FieldName: fieldID, FieldType: tcvectordb.String, IndexType: tcvectordb.PRIMARY},
|
|
{FieldName: fieldContent, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldSourceID, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldSourceType, FieldType: tcvectordb.Uint64, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldChunkID, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldKnowledgeID, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldKnowledgeBaseID, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldTagID, FieldType: tcvectordb.String, IndexType: tcvectordb.FILTER},
|
|
{FieldName: fieldIsEnabled, FieldType: tcvectordb.Uint64, IndexType: tcvectordb.FILTER},
|
|
},
|
|
}
|
|
_, err = r.client.Database(r.databaseName).CreateCollection(
|
|
ctx,
|
|
collectionName,
|
|
uint32(r.shardsNum),
|
|
uint32(r.replicasNum),
|
|
fmt.Sprintf("WeKnora embeddings collection with dimension %d", dimension),
|
|
indexes,
|
|
)
|
|
if err != nil {
|
|
if isCollectionAlreadyExistsErr(err) {
|
|
logger.GetLogger(ctx).Infof("[TencentVectorDB] collection %s already exists, skip create", collectionName)
|
|
r.initialized.Store(dimension, true)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("tencent vectordb create collection %s: %w", collectionName, err)
|
|
}
|
|
|
|
r.initialized.Store(dimension, true)
|
|
return nil
|
|
}
|
|
|
|
func isCollectionAlreadyExistsErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "code: 15202") || strings.Contains(msg, "already exist")
|
|
}
|
|
|
|
func (r *repository) deleteByFilter(ctx context.Context, dimension int, cond string) error {
|
|
if cond == "" {
|
|
return nil
|
|
}
|
|
collectionName := r.collectionName(dimension)
|
|
_, err := r.client.Database(r.databaseName).Collection(collectionName).Delete(ctx, tcvectordb.DeleteDocumentParams{
|
|
Filter: tcvectordb.NewFilter(cond),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb delete from %s: %w", collectionName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) updateChunkFields(ctx context.Context, chunkIDs []string, fields map[string]tcvectordb.Field) error {
|
|
collections, err := r.client.Database(r.databaseName).ListCollection(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb list collections: %w", err)
|
|
}
|
|
|
|
for _, collection := range collections.Collections {
|
|
if !r.matchesCollection(collection.CollectionName) {
|
|
continue
|
|
}
|
|
_, err := r.client.Database(r.databaseName).Collection(collection.CollectionName).Update(ctx, tcvectordb.UpdateDocumentParams{
|
|
QueryFilter: tcvectordb.NewFilter(tcvectordb.In(fieldChunkID, chunkIDs)),
|
|
UpdateFields: fields,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("tencent vectordb update chunks in %s: %w", collection.CollectionName, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) collectionName(dimension int) string {
|
|
if !r.useDimensionSuffix {
|
|
return r.collectionBaseName
|
|
}
|
|
return fmt.Sprintf("%s_%d", r.collectionBaseName, dimension)
|
|
}
|
|
|
|
func (r *repository) matchesCollection(collectionName string) bool {
|
|
if !r.useDimensionSuffix {
|
|
return collectionName == r.collectionBaseName
|
|
}
|
|
return strings.HasPrefix(collectionName, r.collectionBaseName+"_")
|
|
}
|
|
|
|
func shouldUseDimensionSuffix(indexCfg *types.IndexConfig) bool {
|
|
return indexCfg == nil || indexCfg.CollectionName == ""
|
|
}
|
|
|
|
func (r *repository) baseFilter(params types.RetrieveParams) *tcvectordb.Filter {
|
|
conditions := []string{fmt.Sprintf("%s=1", fieldIsEnabled)}
|
|
if len(params.KnowledgeBaseIDs) > 0 {
|
|
conditions = append(conditions, tcvectordb.In(fieldKnowledgeBaseID, params.KnowledgeBaseIDs))
|
|
}
|
|
if len(params.KnowledgeIDs) > 0 {
|
|
conditions = append(conditions, tcvectordb.In(fieldKnowledgeID, params.KnowledgeIDs))
|
|
}
|
|
if len(params.TagIDs) > 0 {
|
|
conditions = append(conditions, tcvectordb.In(fieldTagID, params.TagIDs))
|
|
}
|
|
if len(params.ExcludeKnowledgeIDs) > 0 {
|
|
conditions = append(conditions, tcvectordb.NotIn(fieldKnowledgeID, params.ExcludeKnowledgeIDs))
|
|
}
|
|
if len(params.ExcludeChunkIDs) > 0 {
|
|
conditions = append(conditions, tcvectordb.NotIn(fieldChunkID, params.ExcludeChunkIDs))
|
|
}
|
|
return tcvectordb.NewFilter(strings.Join(conditions, " and "))
|
|
}
|
|
|
|
func (r *repository) retrieveResult(results []*types.IndexWithScore, retrieverType types.RetrieverType) []*types.RetrieveResult {
|
|
return []*types.RetrieveResult{
|
|
{
|
|
Results: results,
|
|
RetrieverEngineType: types.TencentVectorDBRetrieverEngineType,
|
|
RetrieverType: retrieverType,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *repository) bm25Encoder() (encoder.SparseEncoder, error) {
|
|
r.bm25Once.Do(func() {
|
|
r.bm25, r.bm25Err = encoder.NewBM25Encoder(&encoder.BM25EncoderParams{
|
|
Bm25Language: encoder.BM25_ZH_CONTENT,
|
|
})
|
|
if r.bm25Err != nil {
|
|
r.bm25Err = fmt.Errorf("tencent vectordb init BM25 encoder: %w", r.bm25Err)
|
|
}
|
|
})
|
|
return r.bm25, r.bm25Err
|
|
}
|
|
|
|
func (r *repository) toDocumentsWithSparseVectors(
|
|
bm25 encoder.SparseEncoder,
|
|
embeddings []*vectorEmbedding,
|
|
) ([]tcvectordb.Document, error) {
|
|
texts := make([]string, 0, len(embeddings))
|
|
for _, embedding := range embeddings {
|
|
texts = append(texts, embedding.Content)
|
|
}
|
|
sparseVectors, err := bm25.EncodeTexts(texts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tencent vectordb encode BM25 sparse vectors: %w", err)
|
|
}
|
|
docs := make([]tcvectordb.Document, 0, len(embeddings))
|
|
for i, embedding := range embeddings {
|
|
if i >= len(sparseVectors) {
|
|
return nil, fmt.Errorf("tencent vectordb encoded sparse vector count mismatch: got %d, want %d", len(sparseVectors), len(embeddings))
|
|
}
|
|
embedding.SparseVector = sparseVectors[i]
|
|
docs = append(docs, toDocument(embedding))
|
|
}
|
|
return docs, nil
|
|
}
|
|
|
|
func toVectorEmbedding(indexInfo *types.IndexInfo, params map[string]any) *vectorEmbedding {
|
|
embedding := &vectorEmbedding{
|
|
ID: indexInfo.ID,
|
|
Content: cleanInvalidUTF8(indexInfo.Content),
|
|
SourceID: indexInfo.SourceID,
|
|
SourceType: int(indexInfo.SourceType),
|
|
ChunkID: indexInfo.ChunkID,
|
|
KnowledgeID: indexInfo.KnowledgeID,
|
|
KnowledgeBaseID: indexInfo.KnowledgeBaseID,
|
|
TagID: indexInfo.TagID,
|
|
IsEnabled: indexInfo.IsEnabled,
|
|
}
|
|
if embedding.ID == "" {
|
|
embedding.ID = indexInfo.SourceID
|
|
}
|
|
if embedding.ID == "" {
|
|
embedding.ID = indexInfo.ChunkID
|
|
}
|
|
if params != nil && slices.Contains(slices.Collect(maps.Keys(params)), fieldVector) {
|
|
if embeddingMap, ok := params[fieldVector].(map[string][]float32); ok {
|
|
embedding.Embedding = lookupEmbedding(embeddingMap, indexInfo)
|
|
}
|
|
}
|
|
if params != nil && slices.Contains(slices.Collect(maps.Keys(params)), "embedding") {
|
|
if embeddingMap, ok := params["embedding"].(map[string][]float32); ok {
|
|
embedding.Embedding = lookupEmbedding(embeddingMap, indexInfo)
|
|
}
|
|
}
|
|
return embedding
|
|
}
|
|
|
|
func lookupEmbedding(embeddingMap map[string][]float32, indexInfo *types.IndexInfo) []float32 {
|
|
if embedding, ok := embeddingMap[indexInfo.SourceID]; ok {
|
|
return embedding
|
|
}
|
|
return embeddingMap[indexInfo.ChunkID]
|
|
}
|
|
|
|
func copySourceQueryParams(sourceKnowledgeBaseID string, chunkIDs []string, offset int64) *tcvectordb.QueryDocumentParams {
|
|
conditions := []string{tcvectordb.In(fieldChunkID, chunkIDs)}
|
|
if sourceKnowledgeBaseID != "" {
|
|
conditions = append([]string{tcvectordb.In(fieldKnowledgeBaseID, []string{sourceKnowledgeBaseID})}, conditions...)
|
|
}
|
|
return &tcvectordb.QueryDocumentParams{
|
|
Filter: tcvectordb.NewFilter(strings.Join(conditions, " and ")),
|
|
RetrieveVector: true,
|
|
OutputFields: outputFields(),
|
|
Offset: offset,
|
|
Limit: copyIndicesQueryPageSize,
|
|
}
|
|
}
|
|
|
|
func remapCopiedEmbeddings(
|
|
docs []tcvectordb.Document,
|
|
sourceToTargetKBIDMap map[string]string,
|
|
sourceToTargetChunkIDMap map[string]string,
|
|
targetKnowledgeBaseID string,
|
|
) []*vectorEmbedding {
|
|
embeddings := make([]*vectorEmbedding, 0, len(docs))
|
|
for _, doc := range docs {
|
|
embedding := fromDocument(doc)
|
|
targetChunkID := sourceToTargetChunkIDMap[embedding.ChunkID]
|
|
if targetChunkID == "" {
|
|
continue
|
|
}
|
|
originalSourceID := embedding.SourceID
|
|
if originalSourceID == "" {
|
|
originalSourceID = embedding.ID
|
|
}
|
|
targetSourceID := translateSourceID(originalSourceID, embedding.ChunkID, targetChunkID)
|
|
embedding.ID = targetSourceID
|
|
embedding.SourceID = targetSourceID
|
|
embedding.ChunkID = targetChunkID
|
|
embedding.KnowledgeBaseID = targetKnowledgeBaseID
|
|
if targetKBID := sourceToTargetKBIDMap[embedding.KnowledgeID]; targetKBID != "" {
|
|
embedding.KnowledgeID = targetKBID
|
|
}
|
|
embeddings = append(embeddings, embedding)
|
|
}
|
|
return embeddings
|
|
}
|
|
|
|
func translateSourceID(originalSourceID, sourceChunkID, targetChunkID string) string {
|
|
switch {
|
|
case originalSourceID == sourceChunkID:
|
|
return targetChunkID
|
|
case strings.HasPrefix(originalSourceID, sourceChunkID+"-"):
|
|
questionID := strings.TrimPrefix(originalSourceID, sourceChunkID+"-")
|
|
return fmt.Sprintf("%s-%s", targetChunkID, questionID)
|
|
default:
|
|
sum := sha256.Sum256([]byte(targetChunkID + "\x00" + sourceChunkID + "\x00" + originalSourceID))
|
|
return fmt.Sprintf("%s-%s", targetChunkID, hex.EncodeToString(sum[:])[:16])
|
|
}
|
|
}
|
|
|
|
func cleanInvalidUTF8(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s))
|
|
|
|
for i := 0; i < len(s); {
|
|
r, size := utf8.DecodeRuneInString(s[i:])
|
|
if r == utf8.RuneError && size == 1 {
|
|
i++
|
|
continue
|
|
}
|
|
if r == 0 {
|
|
i += size
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
i += size
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func toDocument(embedding *vectorEmbedding) tcvectordb.Document {
|
|
return tcvectordb.Document{
|
|
Id: embedding.ID,
|
|
Vector: embedding.Embedding,
|
|
SparseVector: embedding.SparseVector,
|
|
Fields: map[string]tcvectordb.Field{
|
|
fieldContent: {Val: embedding.Content},
|
|
fieldSourceID: {Val: embedding.SourceID},
|
|
fieldSourceType: {Val: uint64(embedding.SourceType)},
|
|
fieldChunkID: {Val: embedding.ChunkID},
|
|
fieldKnowledgeID: {Val: embedding.KnowledgeID},
|
|
fieldKnowledgeBaseID: {Val: embedding.KnowledgeBaseID},
|
|
fieldTagID: {Val: embedding.TagID},
|
|
fieldIsEnabled: {Val: boolToUint64(embedding.IsEnabled)},
|
|
},
|
|
}
|
|
}
|
|
|
|
func fromDocument(doc tcvectordb.Document) *vectorEmbedding {
|
|
return &vectorEmbedding{
|
|
ID: doc.Id,
|
|
Content: fieldString(doc, fieldContent),
|
|
SourceID: fieldString(doc, fieldSourceID),
|
|
SourceType: int(fieldUint64(doc, fieldSourceType)),
|
|
ChunkID: fieldString(doc, fieldChunkID),
|
|
KnowledgeID: fieldString(doc, fieldKnowledgeID),
|
|
KnowledgeBaseID: fieldString(doc, fieldKnowledgeBaseID),
|
|
TagID: fieldString(doc, fieldTagID),
|
|
Embedding: doc.Vector,
|
|
SparseVector: doc.SparseVector,
|
|
IsEnabled: fieldUint64(doc, fieldIsEnabled) == 1,
|
|
Score: float64(doc.Score),
|
|
}
|
|
}
|
|
|
|
func toIndexWithScore(embedding *vectorEmbedding, matchType types.MatchType) *types.IndexWithScore {
|
|
return &types.IndexWithScore{
|
|
ID: embedding.ID,
|
|
Content: embedding.Content,
|
|
SourceID: embedding.SourceID,
|
|
SourceType: types.SourceType(embedding.SourceType),
|
|
ChunkID: embedding.ChunkID,
|
|
KnowledgeID: embedding.KnowledgeID,
|
|
KnowledgeBaseID: embedding.KnowledgeBaseID,
|
|
TagID: embedding.TagID,
|
|
Score: embedding.Score,
|
|
MatchType: matchType,
|
|
IsEnabled: embedding.IsEnabled,
|
|
}
|
|
}
|
|
|
|
func outputFields() []string {
|
|
return []string{
|
|
fieldID,
|
|
fieldContent,
|
|
fieldSourceID,
|
|
fieldSourceType,
|
|
fieldChunkID,
|
|
fieldKnowledgeID,
|
|
fieldKnowledgeBaseID,
|
|
fieldTagID,
|
|
fieldIsEnabled,
|
|
}
|
|
}
|
|
|
|
func fieldString(doc tcvectordb.Document, name string) string {
|
|
field, ok := doc.Fields[name]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return field.String()
|
|
}
|
|
|
|
func fieldUint64(doc tcvectordb.Document, name string) uint64 {
|
|
field, ok := doc.Fields[name]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return field.Uint64()
|
|
}
|
|
|
|
func boolToUint64(v bool) uint64 {
|
|
if v {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func defaultIfZero(v, def int) int {
|
|
if v <= 0 {
|
|
return def
|
|
}
|
|
return v
|
|
}
|