c27740f739
- Added blur handling for the textarea in the Input-field component to prevent errors when navigating away from the component. - Improved the sanitization process in the AgentStreamDisplay component to preserve specific tags during markdown processing, ensuring accurate content display. - Introduced new methods for handling FAQ-specific post-processing in the knowledge base service, optimizing retrieval and filtering of search results. These changes enhance user experience by improving input handling and content rendering in the chat interface.
254 lines
9.6 KiB
Go
254 lines
9.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/Tencent/WeKnora/internal/application/service/retriever"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/models/embedding"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
)
|
|
|
|
// GetQueryEmbedding computes the query embedding using the embedding model
|
|
// associated with the given knowledge base. Callers can pre-compute and reuse
|
|
// the result across multiple KBs that share the same embedding model to avoid
|
|
// redundant embedding API calls.
|
|
func (s *knowledgeBaseService) GetQueryEmbedding(ctx context.Context, kbID string, queryText string) ([]float32, error) {
|
|
kb, err := s.repo.GetKnowledgeBaseByID(ctx, kbID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentTenantID := types.MustTenantIDFromContext(ctx)
|
|
var embeddingModel embedding.Embedder
|
|
|
|
if kb.TenantID != currentTenantID {
|
|
embeddingModel, err = s.modelService.GetEmbeddingModelForTenant(ctx, kb.EmbeddingModelID, kb.TenantID)
|
|
} else {
|
|
embeddingModel, err = s.modelService.GetEmbeddingModel(ctx, kb.EmbeddingModelID)
|
|
}
|
|
if err != nil {
|
|
logger.Errorf(ctx, "GetQueryEmbedding: failed to get embedding model %s: %v", kb.EmbeddingModelID, err)
|
|
return nil, err
|
|
}
|
|
|
|
return embeddingModel.Embed(ctx, queryText)
|
|
}
|
|
|
|
// ResolveEmbeddingModelKeys resolves embedding model IDs to their actual model
|
|
// identity key (name + endpoint). KBs using the same underlying model across
|
|
// different tenants will share the same key, enabling optimal grouping.
|
|
func (s *knowledgeBaseService) ResolveEmbeddingModelKeys(ctx context.Context, kbs []*types.KnowledgeBase) map[string]string {
|
|
type modelRef struct {
|
|
ModelID string
|
|
TenantID uint64
|
|
}
|
|
|
|
// Deduplicate model references
|
|
uniqueRefs := make(map[modelRef]struct{})
|
|
kbRefs := make(map[string]modelRef, len(kbs))
|
|
for _, kb := range kbs {
|
|
ref := modelRef{ModelID: kb.EmbeddingModelID, TenantID: kb.TenantID}
|
|
uniqueRefs[ref] = struct{}{}
|
|
kbRefs[kb.ID] = ref
|
|
}
|
|
|
|
// Resolve each unique (modelID, tenantID) to a model identity key
|
|
resolvedKeys := make(map[modelRef]string, len(uniqueRefs))
|
|
for ref := range uniqueRefs {
|
|
tenantCtx := context.WithValue(ctx, types.TenantIDContextKey, ref.TenantID)
|
|
model, err := s.modelService.GetModelByID(tenantCtx, ref.ModelID)
|
|
if err != nil || model == nil {
|
|
logger.Warnf(ctx, "ResolveEmbeddingModelKeys: cannot resolve model %s for tenant %d: %v", ref.ModelID, ref.TenantID, err)
|
|
resolvedKeys[ref] = ref.ModelID
|
|
continue
|
|
}
|
|
resolvedKeys[ref] = model.Name + "|" + model.Parameters.BaseURL
|
|
}
|
|
|
|
result := make(map[string]string, len(kbs))
|
|
for _, kb := range kbs {
|
|
result[kb.ID] = resolvedKeys[kbRefs[kb.ID]]
|
|
}
|
|
return result
|
|
}
|
|
|
|
// HybridSearch performs hybrid search, including vector retrieval and keyword retrieval.
|
|
//
|
|
// id is the "primary" knowledge base ID used to resolve the embedding model and
|
|
// determine the KB type (e.g. FAQ). When params.KnowledgeBaseIDs is set, those
|
|
// IDs are used for the actual retrieval scope instead of id alone, allowing a
|
|
// single call to span multiple KBs that share the same embedding model. In that
|
|
// case id should be any one of those KBs (typically the first) so that its
|
|
// embedding model and type configuration are used for the search.
|
|
func (s *knowledgeBaseService) HybridSearch(ctx context.Context,
|
|
id string,
|
|
params types.SearchParams,
|
|
) ([]*types.SearchResult, error) {
|
|
// Determine the set of KB IDs to search
|
|
searchKBIDs := params.KnowledgeBaseIDs
|
|
if len(searchKBIDs) == 0 {
|
|
searchKBIDs = []string{id}
|
|
}
|
|
|
|
logger.Infof(ctx, "Hybrid search parameters, knowledge base IDs: %v, query text: %s", searchKBIDs, params.QueryText)
|
|
|
|
tenantInfo, _ := types.TenantInfoFromContext(ctx)
|
|
|
|
// Create a composite retrieval engine with tenant's configured retrievers
|
|
retrieveEngine, err := retriever.NewCompositeRetrieveEngine(s.retrieveEngine, tenantInfo.GetEffectiveEngines())
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to create retrieval engine: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
kb, err := s.repo.GetKnowledgeBaseByID(ctx, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"knowledge_base_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
// Use 5x over-retrieval to ensure sufficient candidates for RRF fusion and reranking.
|
|
// Scale proportionally when searching multiple KBs to maintain per-KB recall quality.
|
|
matchCount := max(params.MatchCount*5, 50) * len(searchKBIDs)
|
|
if matchCount > 1000 {
|
|
matchCount = 1000
|
|
}
|
|
|
|
// Build retrieval parameters for vector and keyword engines
|
|
retrieveParams, err := s.buildRetrievalParams(ctx, retrieveEngine, kb, params, searchKBIDs, matchCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(retrieveParams) == 0 {
|
|
logger.Error(ctx, "No retrieval parameters available")
|
|
return nil, errors.New("no retrieve params")
|
|
}
|
|
|
|
// Execute retrieval using the configured engines
|
|
logger.Infof(ctx, "Starting retrieval, parameter count: %d", len(retrieveParams))
|
|
retrieveResults, err := retrieveEngine.Retrieve(ctx, retrieveParams)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"knowledge_base_ids": searchKBIDs,
|
|
"query_text": params.QueryText,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
// Separate and fuse retrieval results
|
|
vectorResults, keywordResults := classifyRetrievalResults(ctx, retrieveResults)
|
|
if len(vectorResults) == 0 && len(keywordResults) == 0 {
|
|
logger.Info(ctx, "No search results found")
|
|
return nil, nil
|
|
}
|
|
logger.Infof(ctx, "Result count before fusion: vector=%d, keyword=%d", len(vectorResults), len(keywordResults))
|
|
|
|
deduplicatedChunks := fuseOrDeduplicate(ctx, vectorResults, keywordResults)
|
|
|
|
kb.EnsureDefaults()
|
|
|
|
// FAQ-specific post-processing: iterative retrieval or negative question filtering
|
|
deduplicatedChunks = s.applyFAQPostProcessing(ctx, kb, deduplicatedChunks, vectorResults, retrieveEngine, retrieveParams, params, matchCount)
|
|
|
|
// Limit to MatchCount
|
|
if len(deduplicatedChunks) > params.MatchCount {
|
|
deduplicatedChunks = deduplicatedChunks[:params.MatchCount]
|
|
}
|
|
|
|
return s.processSearchResults(ctx, deduplicatedChunks, params.SkipContextEnrichment)
|
|
}
|
|
|
|
// buildRetrievalParams constructs the vector and keyword retrieval parameters
|
|
// based on the knowledge base type, engine capabilities, and search params.
|
|
func (s *knowledgeBaseService) buildRetrievalParams(
|
|
ctx context.Context,
|
|
retrieveEngine *retriever.CompositeRetrieveEngine,
|
|
kb *types.KnowledgeBase,
|
|
params types.SearchParams,
|
|
searchKBIDs []string,
|
|
matchCount int,
|
|
) ([]types.RetrieveParams, error) {
|
|
currentTenantID := types.MustTenantIDFromContext(ctx)
|
|
var retrieveParams []types.RetrieveParams
|
|
|
|
// Add vector retrieval params if supported
|
|
if retrieveEngine.SupportRetriever(types.VectorRetrieverType) && !params.DisableVectorMatch {
|
|
logger.Info(ctx, "Vector retrieval supported, preparing vector retrieval parameters")
|
|
|
|
var queryEmbedding []float32
|
|
|
|
if len(params.QueryEmbedding) > 0 {
|
|
queryEmbedding = params.QueryEmbedding
|
|
logger.Infof(ctx, "Using pre-computed query embedding, vector length: %d", len(queryEmbedding))
|
|
} else {
|
|
logger.Infof(ctx, "Getting embedding model, model ID: %s", kb.EmbeddingModelID)
|
|
|
|
// Check if this is a cross-tenant shared knowledge base
|
|
// For shared KB, we must use the source tenant's embedding model to ensure vector compatibility
|
|
var embeddingModel embedding.Embedder
|
|
var err error
|
|
if kb.TenantID != currentTenantID {
|
|
logger.Infof(ctx, "Cross-tenant knowledge base detected, using source tenant's embedding model. KB tenant: %d, current tenant: %d", kb.TenantID, currentTenantID)
|
|
embeddingModel, err = s.modelService.GetEmbeddingModelForTenant(ctx, kb.EmbeddingModelID, kb.TenantID)
|
|
} else {
|
|
embeddingModel, err = s.modelService.GetEmbeddingModel(ctx, kb.EmbeddingModelID)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to get embedding model, model ID: %s, error: %v", kb.EmbeddingModelID, err)
|
|
return nil, err
|
|
}
|
|
logger.Infof(ctx, "Embedding model retrieved: %v", embeddingModel)
|
|
|
|
logger.Info(ctx, "Starting to generate query embedding")
|
|
queryEmbedding, err = embeddingModel.Embed(ctx, params.QueryText)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to embed query text, query text: %s, error: %v", params.QueryText, err)
|
|
return nil, err
|
|
}
|
|
logger.Infof(ctx, "Query embedding generated successfully, embedding vector length: %d", len(queryEmbedding))
|
|
}
|
|
|
|
vectorParams := types.RetrieveParams{
|
|
Query: params.QueryText,
|
|
Embedding: queryEmbedding,
|
|
KnowledgeBaseIDs: searchKBIDs,
|
|
TopK: matchCount,
|
|
Threshold: params.VectorThreshold,
|
|
RetrieverType: types.VectorRetrieverType,
|
|
KnowledgeIDs: params.KnowledgeIDs,
|
|
TagIDs: params.TagIDs,
|
|
}
|
|
|
|
// For FAQ knowledge base, use FAQ index
|
|
if kb.Type == types.KnowledgeBaseTypeFAQ {
|
|
vectorParams.KnowledgeType = types.KnowledgeTypeFAQ
|
|
}
|
|
|
|
retrieveParams = append(retrieveParams, vectorParams)
|
|
logger.Info(ctx, "Vector retrieval parameters setup completed")
|
|
}
|
|
|
|
// Add keyword retrieval params if supported and not FAQ
|
|
if retrieveEngine.SupportRetriever(types.KeywordsRetrieverType) && !params.DisableKeywordsMatch &&
|
|
kb.Type != types.KnowledgeBaseTypeFAQ {
|
|
logger.Info(ctx, "Keyword retrieval supported, preparing keyword retrieval parameters")
|
|
retrieveParams = append(retrieveParams, types.RetrieveParams{
|
|
Query: params.QueryText,
|
|
KnowledgeBaseIDs: searchKBIDs,
|
|
TopK: matchCount,
|
|
Threshold: params.KeywordThreshold,
|
|
RetrieverType: types.KeywordsRetrieverType,
|
|
KnowledgeIDs: params.KnowledgeIDs,
|
|
TagIDs: params.TagIDs,
|
|
})
|
|
logger.Info(ctx, "Keyword retrieval parameters setup completed")
|
|
}
|
|
|
|
return retrieveParams, nil
|
|
}
|