diff --git a/core/http/endpoints/localai/agent_collections.go b/core/http/endpoints/localai/agent_collections.go index 539accff50..b2ed40e383 100644 --- a/core/http/endpoints/localai/agent_collections.go +++ b/core/http/endpoints/localai/agent_collections.go @@ -11,7 +11,7 @@ import ( // ListCollectionsEndpoint handles /api/collections (GET). // @Summary List your collections. -// @Description The knowledge-base collections of the caller. An administrator can add all_users=true to also get the other users' collections in user_groups. embedding_model names the model that indexes and searches the collections (collections_embedding_model), and embedding_model_installed says whether this server has it: without it, adding files and searching fail. Needs the collections feature. +// @Description The knowledge-base collections of the caller. An administrator can add all_users=true to also get the other users' collections in user_groups. embedding_model names the model that indexes and searches the collections (collections_embedding_model), and embedding_model_installed says whether this server has it: without it, adding files and searching fail. search_mode says how a search finds passages: semantic (by meaning only) or hybrid (by meaning and by keyword). Needs the collections feature. // @Tags collections // @Success 200 {object} schema.CollectionList "The collections" // @Router /api/collections [get] @@ -30,6 +30,7 @@ func ListCollectionsEndpoint(app *application.Application) echo.HandlerFunc { "count": len(cols), "embedding_model": model, "embedding_model_installed": embeddingModelInstalled(app, model), + "search_mode": collectionSearchMode(app.ApplicationConfig().Collections.VectorEngine), } // Admin cross-user aggregation @@ -57,6 +58,16 @@ func ListCollectionsEndpoint(app *application.Application) echo.HandlerFunc { } } +// collectionSearchMode says how a search of the vector engine finds passages. +// The chromem and localai engines compare embeddings only. The postgres engine +// also matches words (BM25) and mixes the two lists. +func collectionSearchMode(engine string) string { + if engine == "postgres" { + return "hybrid" + } + return "semantic" +} + // CreateCollectionEndpoint handles /api/collections (POST). // @Summary Create a collection. // @Description Makes an empty collection under the name in the body. diff --git a/core/http/endpoints/localai/collections_embedding_internal_test.go b/core/http/endpoints/localai/collections_embedding_internal_test.go index e227cf1dad..599bc7a30b 100644 --- a/core/http/endpoints/localai/collections_embedding_internal_test.go +++ b/core/http/endpoints/localai/collections_embedding_internal_test.go @@ -48,3 +48,15 @@ var _ = Describe("the embedding model of the collections", func() { Expect(embeddingModelInstalled(app, "plain.gguf")).To(BeTrue()) }) }) + +var _ = Describe("the search mode of the collections", func() { + It("is semantic for the engines that compare embeddings only", func() { + Expect(collectionSearchMode("chromem")).To(Equal("semantic")) + Expect(collectionSearchMode("localai")).To(Equal("semantic")) + Expect(collectionSearchMode("")).To(Equal("semantic")) + }) + + It("is hybrid for the postgres engine, which also matches words", func() { + Expect(collectionSearchMode("postgres")).To(Equal("hybrid")) + }) +}) diff --git a/core/http/react-ui/e2e/collections.spec.js b/core/http/react-ui/e2e/collections.spec.js index cc9909939f..1e8df1c749 100644 --- a/core/http/react-ui/e2e/collections.spec.js +++ b/core/http/react-ui/e2e/collections.spec.js @@ -27,6 +27,30 @@ test.describe('Collections page', () => { await expect(page.getByRole('button', { name: 'New collection' }).first()).toBeVisible() }) + test('says how a search finds passages, in plain words', async ({ page }) => { + const list = mode => route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ collections: ['docs'], search_mode: mode, embedding_model_installed: true }) }) + await page.route(/\/api\/collections(\?.*)?$/, list('semantic')) + await page.goto('/app/collections') + await expect(page.getByTestId('search-mode')).toContainText('Search: semantic.') + await expect(page.getByTestId('search-mode')).toContainText('by meaning, not by exact words or codes') + + await page.unroute(/\/api\/collections(\?.*)?$/) + await page.route(/\/api\/collections(\?.*)?$/, list('hybrid')) + await page.goto('/app/collections') + await expect(page.getByTestId('search-mode')).toContainText('Search: hybrid.') + await expect(page.getByTestId('search-mode')).toContainText('by meaning and by exact words') + + await page.setViewportSize({ width: 360, height: 740 }) + await expect(page.getByTestId('search-mode')).toBeVisible() + // The page animates in; wait for the layout to settle before measuring. + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(0) + }) + + test('says nothing about the search mode when the server does not report it', async ({ page }) => { + await expect(page.getByTestId('memory-empty')).toBeVisible() + await expect(page.getByTestId('search-mode')).toHaveCount(0) + }) + test('new-collection name field accepts input', async ({ page }) => { await page.getByRole('button', { name: 'New collection' }).first().click() const input = page.getByLabel('New collection name') diff --git a/core/http/react-ui/public/locales/de/collections.json b/core/http/react-ui/public/locales/de/collections.json index 77b25adeea..89daadb14f 100644 --- a/core/http/react-ui/public/locales/de/collections.json +++ b/core/http/react-ui/public/locales/de/collections.json @@ -57,5 +57,12 @@ "noneSet": "Für Sammlungen ist kein Embedding-Modell festgelegt (collections_embedding_model), daher schlagen das Hinzufügen von Dateien und die Suche fehl.", "install": "In Modelle suchen", "askAdmin": "Bitte eine Administratorin oder einen Administrator, es zu installieren." + }, + "searchMode": { + "label": "Suche: {{mode}}", + "semantic": "semantisch", + "hybrid": "hybrid", + "semanticHint": "Sie findet Textstellen nach Bedeutung, nicht nach genauen Wörtern oder Codes.", + "hybridHint": "Sie findet Textstellen nach Bedeutung und nach genauen Wörtern." } } diff --git a/core/http/react-ui/public/locales/en/collections.json b/core/http/react-ui/public/locales/en/collections.json index e9fa9f0017..ec15b1395e 100644 --- a/core/http/react-ui/public/locales/en/collections.json +++ b/core/http/react-ui/public/locales/en/collections.json @@ -146,5 +146,12 @@ "noneSet": "No embedding model is set for collections (collections_embedding_model), so adding files and searching fail.", "install": "Find it in Models", "askAdmin": "Ask an administrator to install it." + }, + "searchMode": { + "label": "Search: {{mode}}", + "semantic": "semantic", + "hybrid": "hybrid", + "semanticHint": "It finds passages by meaning, not by exact words or codes.", + "hybridHint": "It finds passages by meaning and by exact words." } } diff --git a/core/http/react-ui/public/locales/es/collections.json b/core/http/react-ui/public/locales/es/collections.json index cf397f7c89..aaf7961a8a 100644 --- a/core/http/react-ui/public/locales/es/collections.json +++ b/core/http/react-ui/public/locales/es/collections.json @@ -57,5 +57,12 @@ "noneSet": "No hay ningún modelo de embeddings configurado para las colecciones (collections_embedding_model), así que añadir archivos y buscar fallan.", "install": "Buscarlo en Modelos", "askAdmin": "Pide a un administrador que lo instale." + }, + "searchMode": { + "label": "Búsqueda: {{mode}}", + "semantic": "semántica", + "hybrid": "híbrida", + "semanticHint": "Encuentra pasajes por su significado, no por palabras o códigos exactos.", + "hybridHint": "Encuentra pasajes por su significado y por palabras exactas." } } diff --git a/core/http/react-ui/public/locales/fr/collections.json b/core/http/react-ui/public/locales/fr/collections.json index 5c6806207d..73758b3f22 100644 --- a/core/http/react-ui/public/locales/fr/collections.json +++ b/core/http/react-ui/public/locales/fr/collections.json @@ -57,5 +57,12 @@ "noneSet": "Aucun modèle d'embedding n'est défini pour les collections (collections_embedding_model) : l'ajout de fichiers et la recherche échouent.", "install": "Le trouver dans Modèles", "askAdmin": "Demandez à un administrateur de l'installer." + }, + "searchMode": { + "label": "Recherche : {{mode}}", + "semantic": "sémantique", + "hybrid": "hybride", + "semanticHint": "Elle trouve des passages par le sens, pas par des mots ou des codes exacts.", + "hybridHint": "Elle trouve des passages par le sens et par des mots exacts." } } diff --git a/core/http/react-ui/public/locales/id/collections.json b/core/http/react-ui/public/locales/id/collections.json index 67790cf45b..b9513ab76b 100644 --- a/core/http/react-ui/public/locales/id/collections.json +++ b/core/http/react-ui/public/locales/id/collections.json @@ -57,5 +57,12 @@ "noneSet": "Tidak ada model embedding yang diatur untuk koleksi (collections_embedding_model), jadi menambah file dan pencarian gagal.", "install": "Cari di Model", "askAdmin": "Minta administrator untuk memasangnya." + }, + "searchMode": { + "label": "Pencarian: {{mode}}", + "semantic": "semantik", + "hybrid": "hibrida", + "semanticHint": "Pencarian menemukan bagian teks berdasarkan makna, bukan kata atau kode yang persis.", + "hybridHint": "Pencarian menemukan bagian teks berdasarkan makna dan kata yang persis." } } diff --git a/core/http/react-ui/public/locales/it/collections.json b/core/http/react-ui/public/locales/it/collections.json index f3a545613e..18ef7031f4 100644 --- a/core/http/react-ui/public/locales/it/collections.json +++ b/core/http/react-ui/public/locales/it/collections.json @@ -57,5 +57,12 @@ "noneSet": "Nessun modello di embedding è impostato per le raccolte (collections_embedding_model), quindi l'aggiunta di file e la ricerca non funzionano.", "install": "Trovalo in Modelli", "askAdmin": "Chiedi a un amministratore di installarlo." + }, + "searchMode": { + "label": "Ricerca: {{mode}}", + "semantic": "semantica", + "hybrid": "ibrida", + "semanticHint": "Trova i passaggi per significato, non per parole o codici esatti.", + "hybridHint": "Trova i passaggi per significato e per parole esatte." } } diff --git a/core/http/react-ui/public/locales/ko/collections.json b/core/http/react-ui/public/locales/ko/collections.json index 903ed40d3e..f33cd34f70 100644 --- a/core/http/react-ui/public/locales/ko/collections.json +++ b/core/http/react-ui/public/locales/ko/collections.json @@ -57,5 +57,12 @@ "noneSet": "컬렉션에 임베딩 모델이 설정되어 있지 않아(collections_embedding_model) 파일 추가와 검색이 실패합니다.", "install": "모델에서 찾기", "askAdmin": "관리자에게 설치를 요청하세요." + }, + "searchMode": { + "label": "검색: {{mode}}", + "semantic": "의미 기반", + "hybrid": "하이브리드", + "semanticHint": "정확한 단어나 코드가 아니라 의미로 구절을 찾습니다.", + "hybridHint": "의미와 정확한 단어로 구절을 찾습니다." } } diff --git a/core/http/react-ui/public/locales/pt-BR/collections.json b/core/http/react-ui/public/locales/pt-BR/collections.json index ebcbd32cc7..477b367257 100644 --- a/core/http/react-ui/public/locales/pt-BR/collections.json +++ b/core/http/react-ui/public/locales/pt-BR/collections.json @@ -57,5 +57,12 @@ "noneSet": "Nenhum modelo de embeddings está definido para as coleções (collections_embedding_model), então adicionar arquivos e pesquisar falham.", "install": "Encontrar em Modelos", "askAdmin": "Peça a um administrador para instalá-lo." + }, + "searchMode": { + "label": "Busca: {{mode}}", + "semantic": "semântica", + "hybrid": "híbrida", + "semanticHint": "Encontra trechos pelo significado, não por palavras ou códigos exatos.", + "hybridHint": "Encontra trechos pelo significado e por palavras exatas." } } diff --git a/core/http/react-ui/public/locales/zh-CN/collections.json b/core/http/react-ui/public/locales/zh-CN/collections.json index 7ff5d0070f..b14b0d1379 100644 --- a/core/http/react-ui/public/locales/zh-CN/collections.json +++ b/core/http/react-ui/public/locales/zh-CN/collections.json @@ -57,5 +57,12 @@ "noneSet": "未为集合设置嵌入模型(collections_embedding_model),因此添加文件和搜索都会失败。", "install": "在模型中查找", "askAdmin": "请让管理员安装它。" + }, + "searchMode": { + "label": "搜索:{{mode}}", + "semantic": "语义", + "hybrid": "混合", + "semanticHint": "按含义查找段落,不按精确的词或代码匹配。", + "hybridHint": "按含义和精确的词查找段落。" } } diff --git a/core/http/react-ui/src/pages/Collections.jsx b/core/http/react-ui/src/pages/Collections.jsx index 8d8f2b27c0..34fe678337 100644 --- a/core/http/react-ui/src/pages/Collections.jsx +++ b/core/http/react-ui/src/pages/Collections.jsx @@ -60,6 +60,9 @@ export default function Collections() { // The model that indexes and searches the collections, and whether this // server has it. Without it every upload and search fails. const [embedding, setEmbedding] = useState({ model: '', installed: true }) + // How a search finds passages: 'semantic' or 'hybrid'. '' while unknown + // (an older server does not say). + const [searchMode, setSearchMode] = useState('') const selectedUser = params.get('user_id') || undefined @@ -74,6 +77,7 @@ export default function Collections() { setCollections((Array.isArray(data.collections) ? data.collections : []).map(nameOf).filter(Boolean)) setUserGroups(data.user_groups || null) setEmbedding({ model: data.embedding_model || '', installed: data.embedding_model_installed !== false }) + setSearchMode(data.search_mode === 'hybrid' || data.search_mode === 'semantic' ? data.search_mode : '') setLoadError(null) } catch (err) { // A failed read shows an error in place of the list, never the empty @@ -257,6 +261,13 @@ export default function Collections() { )} + {!loading && !loadError && searchMode && ( +
+ {t('collections:searchMode.label', { mode: t(`collections:searchMode.${searchMode}`) })}.{' '} + {t(`collections:searchMode.${searchMode}Hint`)} +
+ )} + {!loading && !loadError && !embedding.installed && (