fix(rag,desktop): Milvus 集合丢失自愈 + 检索框布局塌陷
真实演示中暴露的两个 bug: 1) Milvus 重连健壮性(mcp-go/internal/rag/milvus.go) 基础设施重启后向量集合丢失,但 ensure() 的 m.ok 缓存认定集合仍在、跳过重建, 导致 insert/search 报 "collection not found",必须重启进程才恢复。 修复:新增 invalidate() + isCollectionGone();insert/search 遇"集合不存在"类错误 时清缓存 + 重 ensure(重建集合)+ 重试一次。 实测:运行期 drop 集合后再入库 → 日志"清缓存重建后重试写入" → 写入成功且可检索(自愈,无需重启)。 2) 检索框布局塌陷(desktop frontend/src/ui/Input.tsx) Phase A 给 Input 基类内置了 w-full,与检索行调用方的 flex-1 / w-16 冲突, 查询框被挤成一条缝。修复:基类去掉 w-full,宽度交由调用方(Field 内 flex-col 自动撑满, 或显式 w-full/flex-1/w-16)。 实测(Preview):查询框 412px、topK 64px;报告页输入仍撑满(764/220px),无回归。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import type { InputHTMLAttributes, TextareaHTMLAttributes, SelectHTMLAttributes, ReactNode } from "react";
|
import type { InputHTMLAttributes, TextareaHTMLAttributes, SelectHTMLAttributes, ReactNode } from "react";
|
||||||
import { cn } from "./cn";
|
import { cn } from "./cn";
|
||||||
|
|
||||||
|
// 注意:基类不含宽度。宽度由调用方决定(Field 内为 flex-col 自动撑满,
|
||||||
|
// 或显式 w-full / flex-1 / w-16),避免 w-full 与 flex-1/w-16 冲突塌陷。
|
||||||
const fieldBase =
|
const fieldBase =
|
||||||
"w-full rounded-md border border-line bg-ink-950 text-sm text-slate-200 placeholder:text-slate-600 transition focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand/40 disabled:opacity-50";
|
"rounded-md border border-line bg-ink-950 text-sm text-slate-200 placeholder:text-slate-600 transition focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand/40 disabled:opacity-50";
|
||||||
|
|
||||||
export function Input({ className, ...rest }: InputHTMLAttributes<HTMLInputElement>) {
|
export function Input({ className, ...rest }: InputHTMLAttributes<HTMLInputElement>) {
|
||||||
return <input className={cn(fieldBase, "h-9 px-3", className)} {...rest} />;
|
return <input className={cn(fieldBase, "h-9 px-3", className)} {...rest} />;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/milvus-io/milvus-sdk-go/v2/client"
|
"github.com/milvus-io/milvus-sdk-go/v2/client"
|
||||||
@@ -15,10 +16,10 @@ const collection = "sundynix_wiki" // Wiki/知识库向量集合
|
|||||||
|
|
||||||
// milvusStore 封装 Milvus 连接与集合管理(集合按首次写入的向量维度懒建)。
|
// milvusStore 封装 Milvus 连接与集合管理(集合按首次写入的向量维度懒建)。
|
||||||
type milvusStore struct {
|
type milvusStore struct {
|
||||||
cli client.Client
|
cli client.Client
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
dim int // 已建集合的维度(0=未建)
|
dim int // 已建集合的维度(0=未建)
|
||||||
ok bool // 集合是否就绪
|
ok bool // 集合是否就绪
|
||||||
}
|
}
|
||||||
|
|
||||||
func openMilvus(ctx context.Context, addr string) (*milvusStore, error) {
|
func openMilvus(ctx context.Context, addr string) (*milvusStore, error) {
|
||||||
@@ -79,27 +80,56 @@ func (m *milvusStore) ensure(ctx context.Context, dim int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invalidate 让集合就绪缓存失效(集合被外部删除/基础设施重启丢失后,强制下次重建)。
|
||||||
|
func (m *milvusStore) invalidate() {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.ok = false
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCollectionGone 判断错误是否为"集合不存在"(Milvus 重启丢集合后写/查会报此类)。
|
||||||
|
func isCollectionGone(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(s, "collection not found") ||
|
||||||
|
strings.Contains(s, "can't find collection") ||
|
||||||
|
strings.Contains(s, "collection not exist") ||
|
||||||
|
strings.Contains(s, "collection not loaded")
|
||||||
|
}
|
||||||
|
|
||||||
// insert 写入若干 (kb, text, vector)。
|
// insert 写入若干 (kb, text, vector)。
|
||||||
|
// 若集合在运行期被丢失(如 Milvus 重启)→ 清缓存、重建集合后重试一次,避免必须重启进程才能恢复。
|
||||||
func (m *milvusStore) insert(ctx context.Context, kb string, texts []string, vecs [][]float32) error {
|
func (m *milvusStore) insert(ctx context.Context, kb string, texts []string, vecs [][]float32) error {
|
||||||
if len(vecs) == 0 {
|
if len(vecs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := m.ensure(ctx, len(vecs[0])); err != nil {
|
dim := len(vecs[0])
|
||||||
return err
|
|
||||||
}
|
|
||||||
kbs := make([]string, len(texts))
|
kbs := make([]string, len(texts))
|
||||||
for i := range kbs {
|
for i := range kbs {
|
||||||
kbs[i] = kb
|
kbs[i] = kb
|
||||||
}
|
}
|
||||||
_, err := m.cli.Insert(ctx, collection, "",
|
do := func() error {
|
||||||
entity.NewColumnVarChar("kb", kbs),
|
if err := m.ensure(ctx, dim); err != nil {
|
||||||
entity.NewColumnVarChar("text", texts),
|
return err
|
||||||
entity.NewColumnFloatVector("vector", len(vecs[0]), vecs),
|
}
|
||||||
)
|
if _, err := m.cli.Insert(ctx, collection, "",
|
||||||
if err != nil {
|
entity.NewColumnVarChar("kb", kbs),
|
||||||
return fmt.Errorf("insert: %w", err)
|
entity.NewColumnVarChar("text", texts),
|
||||||
|
entity.NewColumnFloatVector("vector", dim, vecs),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("insert: %w", err)
|
||||||
|
}
|
||||||
|
return m.cli.Flush(ctx, collection, false)
|
||||||
}
|
}
|
||||||
return m.cli.Flush(ctx, collection, false)
|
err := do()
|
||||||
|
if err != nil && isCollectionGone(err) {
|
||||||
|
log.Printf("[rag] 集合不存在(疑似 Milvus 重启丢失),清缓存重建后重试写入")
|
||||||
|
m.invalidate()
|
||||||
|
err = do()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// vectorDim 从集合 schema 读出向量字段维度(用于检测维度变化)。
|
// vectorDim 从集合 schema 读出向量字段维度(用于检测维度变化)。
|
||||||
@@ -125,22 +155,29 @@ type Hit struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// search 用查询向量做 topK 向量检索(可按 kb 过滤)。
|
// search 用查询向量做 topK 向量检索(可按 kb 过滤)。
|
||||||
|
// 集合未建(还没入过库)→ 返回空结果;集合运行期丢失 → 清缓存重建后重试一次。
|
||||||
func (m *milvusStore) search(ctx context.Context, kb string, qvec []float32, topK int) ([]Hit, error) {
|
func (m *milvusStore) search(ctx context.Context, kb string, qvec []float32, topK int) ([]Hit, error) {
|
||||||
if !m.ok {
|
|
||||||
// 集合未建(还没入过库)→ 尝试确保(按查询维度),无则空结果。
|
|
||||||
if err := m.ensure(ctx, len(qvec)); err != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expr := ""
|
expr := ""
|
||||||
if kb != "" {
|
if kb != "" {
|
||||||
expr = fmt.Sprintf("kb == \"%s\"", kb)
|
expr = fmt.Sprintf("kb == \"%s\"", kb)
|
||||||
}
|
}
|
||||||
sp, _ := entity.NewIndexAUTOINDEXSearchParam(1)
|
sp, _ := entity.NewIndexAUTOINDEXSearchParam(1)
|
||||||
results, err := m.cli.Search(ctx, collection, nil, expr, []string{"text"},
|
do := func() ([]client.SearchResult, error) {
|
||||||
[]entity.Vector{entity.FloatVector(qvec)}, "vector", entity.COSINE, topK, sp)
|
if err := m.ensure(ctx, len(qvec)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m.cli.Search(ctx, collection, nil, expr, []string{"text"},
|
||||||
|
[]entity.Vector{entity.FloatVector(qvec)}, "vector", entity.COSINE, topK, sp)
|
||||||
|
}
|
||||||
|
results, err := do()
|
||||||
|
if err != nil && isCollectionGone(err) {
|
||||||
|
log.Printf("[rag] 检索时集合不存在,清缓存重建后重试")
|
||||||
|
m.invalidate()
|
||||||
|
results, err = do()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("search: %w", err)
|
// 集合尚未就绪/无法重建 → 降级空结果(不阻断混合检索其它路)。
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
var hits []Hit
|
var hits []Hit
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
|
|||||||
Reference in New Issue
Block a user