Phase 3.1: Help Center semantic search with pgvector
- ArticleEmbeddingRepo (new): Upsert, GetByArticleID, DeleteByArticleID, SearchByEmbedding using pgvector cosine distance (vector_embedding column) - ArticleEmbedding model: add VectorEmbedding pgvector.Vector field alongside existing JSONB Embedding (backward compatible) - ArticleService: add SemanticSearch() — generates query embedding via LLM, searches articles by cosine similarity; add GenerateEmbedding() — creates and stores article embedding from title+description+content - ArticleHandler: add SemanticSearch endpoint GET /portals/:portal_id/articles/semantic_search?query=... - bootstrap.go: inject articleEmbeddingRepo + llmProvider into ArticleService - router.go: register /articles/semantic_search route - migration 000049: add vector(1536) column to article_embeddings table, create ivfflat index, migrate existing JSONB data to vector format Verified: go build + go vet + go test all pass
This commit is contained in:
@@ -405,6 +405,45 @@ func (h *ArticleHandler) Search(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"payload": articlePayloads(articles), "meta": articleListMetaPayload(meta, p.Page)})
|
||||
}
|
||||
|
||||
// SemanticSearch performs AI-powered semantic search on help center articles.
|
||||
// Uses LLM embeddings + pgvector cosine similarity to find articles by meaning,
|
||||
// not just keyword matching.
|
||||
//
|
||||
// GET /portals/:portal_id/articles/semantic_search?query=how+to+reset+password
|
||||
func (h *ArticleHandler) SemanticSearch(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
portal, ok := h.resolvePortal(c, accountID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
query := strings.TrimSpace(c.Query("query"))
|
||||
if query == "" {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "query parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 50 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
articles, err := h.svc.SemanticSearch(c.Request.Context(), portal.ID, query, limit)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("SemanticSearch articles: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to perform semantic search")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": articlePayloads(articles)})
|
||||
}
|
||||
|
||||
// StatusCounts returns article counts grouped by status.
|
||||
// GET /portals/:portal_id/articles/status_counts
|
||||
func (h *ArticleHandler) StatusCounts(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user