fix: make semantic search dependencies optional extras (#566)

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Hernandez
2026-02-12 21:46:26 -06:00
committed by GitHub
parent 312662f382
commit 1428d18de1
13 changed files with 91 additions and 25 deletions
+3 -3
View File
@@ -55,7 +55,7 @@ jobs:
- name: Install dependencies
run: |
uv pip install -e .[dev]
uv pip install -e ".[dev,semantic]"
- name: Run type checks
run: |
@@ -105,7 +105,7 @@ jobs:
- name: Install dependencies
run: |
uv pip install -e .[dev]
uv pip install -e ".[dev,semantic]"
- name: Run tests (Postgres via testcontainers)
run: |
@@ -141,7 +141,7 @@ jobs:
- name: Install dependencies
run: |
uv pip install -e .[dev]
uv pip install -e ".[dev,semantic]"
- name: Run combined coverage (SQLite + Postgres)
run: |
+54 -4
View File
@@ -12,21 +12,70 @@ Basic Memory's default search uses full-text search (FTS) — keyword matching w
Semantic search is **opt-in** — existing behavior is completely unchanged unless you enable it. It works on both SQLite (local) and Postgres (cloud) backends.
## Installation
Semantic search dependencies (fastembed, sqlite-vec, openai) are **optional extras** — they are not installed with the base `basic-memory` package. Install them with:
```bash
pip install 'basic-memory[semantic]'
```
This keeps the base install lightweight and avoids platform-specific issues with ONNX Runtime wheels.
### Platform Compatibility
| Platform | FastEmbed (local) | OpenAI (API) |
|---|---|---|
| macOS ARM64 (Apple Silicon) | Yes | Yes |
| macOS x86_64 (Intel Mac) | No — see workaround below | Yes |
| Linux x86_64 | Yes | Yes |
| Linux ARM64 | Yes | Yes |
| Windows x86_64 | Yes | Yes |
#### Intel Mac Workaround
The default FastEmbed provider uses ONNX Runtime, which dropped Intel Mac (x86_64) wheels starting in v1.24. Intel Mac users have two options:
**Option 1: Use OpenAI embeddings (recommended)**
Install only the OpenAI dependency manually — no ONNX Runtime or FastEmbed needed:
```bash
pip install openai sqlite-vec
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=openai
export OPENAI_API_KEY=sk-...
```
**Option 2: Pin an older ONNX Runtime**
FastEmbed's ONNX Runtime dependency is unpinned, so you can constrain it to an older version that still ships Intel Mac wheels by passing both requirements in the same install command:
```bash
pip install 'basic-memory[semantic]' 'onnxruntime<1.24'
```
## Quick Start
1. Enable semantic search:
1. Install semantic extras:
```bash
pip install 'basic-memory[semantic]'
```
2. Enable semantic search:
```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
```
2. Build vector embeddings for your existing content:
3. Build vector embeddings for your existing content:
```bash
bm reindex --embeddings
```
3. Search using semantic modes:
4. Search using semantic modes:
```python
# Pure vector similarity
@@ -63,7 +112,8 @@ FastEmbed runs entirely locally using ONNX models — no API key, no network cal
- **Tradeoff**: Smaller model, fast inference, good quality for most use cases
```bash
# FastEmbed is the default — just enable semantic search
# Install semantic extras and enable
pip install 'basic-memory[semantic]'
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
```
+4
View File
@@ -44,6 +44,10 @@ dependencies = [
"sniffio>=1.3.1",
"anyio>=4.10.0",
"httpx>=0.28.0",
]
[project.optional-dependencies]
semantic = [
"fastembed>=0.7.4",
"sqlite-vec>=0.1.6",
"openai>=1.100.2",
+2 -2
View File
@@ -38,14 +38,14 @@ def _format_search_error_response(
`search_notes("{project}", "{query}", search_type="text")`
""").strip()
if "pip install basic-memory" in error_message.lower():
if "pip install" in error_message.lower() and "semantic" in error_message.lower():
return dedent(f"""
# Search Failed - Semantic Dependencies Missing
Semantic retrieval is enabled but required packages are not installed.
## Fix
1. Reinstall basic-memory: `pip install basic-memory`
1. Install semantic extras: `pip install 'basic-memory[semantic]'`
2. Restart Basic Memory
3. Retry your query:
`search_notes("{project}", "{query}", search_type="{search_type}")`
@@ -2,8 +2,6 @@
from basic_memory.config import BasicMemoryConfig
from basic_memory.repository.embedding_provider import EmbeddingProvider
from basic_memory.repository.fastembed_provider import FastEmbedEmbeddingProvider
from basic_memory.repository.openai_provider import OpenAIEmbeddingProvider
def create_embedding_provider(app_config: BasicMemoryConfig) -> EmbeddingProvider:
@@ -18,6 +16,9 @@ def create_embedding_provider(app_config: BasicMemoryConfig) -> EmbeddingProvide
extra_kwargs["dimensions"] = app_config.semantic_embedding_dimensions
if provider_name == "fastembed":
# Deferred import: fastembed (and its onnxruntime dep) may not be installed
from basic_memory.repository.fastembed_provider import FastEmbedEmbeddingProvider
return FastEmbedEmbeddingProvider(
model_name=app_config.semantic_embedding_model,
batch_size=app_config.semantic_embedding_batch_size,
@@ -25,6 +26,9 @@ def create_embedding_provider(app_config: BasicMemoryConfig) -> EmbeddingProvide
)
if provider_name == "openai":
# Deferred import: openai may not be installed
from basic_memory.repository.openai_provider import OpenAIEmbeddingProvider
model_name = app_config.semantic_embedding_model or "text-embedding-3-small"
if model_name == "bge-small-en-v1.5":
model_name = "text-embedding-3-small"
@@ -48,7 +48,7 @@ class FastEmbedEmbeddingProvider(EmbeddingProvider):
) as exc: # pragma: no cover - exercised via tests with monkeypatch
raise SemanticDependenciesMissingError(
"fastembed package is missing. "
"Reinstall basic-memory: pip install basic-memory"
"Install semantic extras: pip install 'basic-memory[semantic]'"
) from exc
resolved_model_name = self._MODEL_ALIASES.get(self.model_name, self.model_name)
return TextEmbedding(model_name=resolved_model_name)
@@ -44,7 +44,8 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
from openai import AsyncOpenAI
except ImportError as exc: # pragma: no cover - covered via monkeypatch tests
raise SemanticDependenciesMissingError(
"OpenAI dependency is missing. Reinstall basic-memory: pip install basic-memory"
"OpenAI dependency is missing. "
"Install semantic extras: pip install 'basic-memory[semantic]'"
) from exc
api_key = self._api_key or os.getenv("OPENAI_API_KEY")
@@ -353,7 +353,8 @@ class SearchRepositoryBase(ABC):
if self._embedding_provider is None:
raise SemanticDependenciesMissingError(
"No embedding provider configured. "
"Ensure semantic_search_enabled is true in your config."
"Install semantic extras: pip install 'basic-memory[semantic]' "
"and set semantic_search_enabled=true."
)
def _compose_row_source_text(self, row) -> str:
@@ -339,7 +339,8 @@ class SQLiteSearchRepository(SearchRepositoryBase):
import sqlite_vec
except ImportError as exc:
raise SemanticDependenciesMissingError(
"sqlite-vec package is missing. Reinstall basic-memory: pip install basic-memory"
"sqlite-vec package is missing. "
"Install semantic extras: pip install 'basic-memory[semantic]'"
) from exc
async with self._sqlite_vec_lock:
+2 -2
View File
@@ -291,13 +291,13 @@ class TestSearchErrorFormatting:
"""Test formatting for missing semantic dependencies."""
result = _format_search_error_response(
"test-project",
"fastembed package is missing. Reinstall basic-memory: pip install basic-memory",
"fastembed package is missing. Install semantic extras: pip install 'basic-memory[semantic]'",
"semantic query",
"hybrid",
)
assert "# Search Failed - Semantic Dependencies Missing" in result
assert "pip install basic-memory" in result
assert "pip install 'basic-memory[semantic]'" in result
def test_format_search_error_generic(self):
"""Test formatting for generic errors."""
+1 -1
View File
@@ -84,4 +84,4 @@ async def test_fastembed_provider_missing_dependency_raises_actionable_error(mon
with pytest.raises(SemanticDependenciesMissingError) as error:
await provider.embed_query("test")
assert "pip install basic-memory" in str(error.value)
assert "pip install 'basic-memory[semantic]'" in str(error.value)
+1 -1
View File
@@ -92,7 +92,7 @@ async def test_openai_provider_missing_dependency_raises_actionable_error(monkey
with pytest.raises(SemanticDependenciesMissingError) as error:
await provider.embed_query("test")
assert "pip install basic-memory" in str(error.value)
assert "pip install 'basic-memory[semantic]'" in str(error.value)
@pytest.mark.asyncio
Generated
+11 -6
View File
@@ -151,7 +151,6 @@ dependencies = [
{ name = "asyncpg" },
{ name = "dateparser" },
{ name = "fastapi", extra = ["standard"] },
{ name = "fastembed" },
{ name = "fastmcp" },
{ name = "greenlet" },
{ name = "httpx" },
@@ -162,7 +161,6 @@ dependencies = [
{ name = "mdformat-frontmatter" },
{ name = "mdformat-gfm" },
{ name = "nest-asyncio" },
{ name = "openai" },
{ name = "pillow" },
{ name = "psycopg" },
{ name = "pybars3" },
@@ -178,12 +176,18 @@ dependencies = [
{ name = "rich" },
{ name = "sniffio" },
{ name = "sqlalchemy" },
{ name = "sqlite-vec" },
{ name = "typer" },
{ name = "unidecode" },
{ name = "watchfiles" },
]
[package.optional-dependencies]
semantic = [
{ name = "fastembed" },
{ name = "openai" },
{ name = "sqlite-vec" },
]
[package.dev-dependencies]
dev = [
{ name = "freezegun" },
@@ -210,7 +214,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.30.0" },
{ name = "dateparser", specifier = ">=1.2.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" },
{ name = "fastembed", specifier = ">=0.7.4" },
{ name = "fastembed", marker = "extra == 'semantic'", specifier = ">=0.7.4" },
{ name = "fastmcp", specifier = "==2.12.3" },
{ name = "greenlet", specifier = ">=3.1.1" },
{ name = "httpx", specifier = ">=0.28.0" },
@@ -221,7 +225,7 @@ requires-dist = [
{ name = "mdformat-frontmatter", specifier = ">=2.0.8" },
{ name = "mdformat-gfm", specifier = ">=0.3.7" },
{ name = "nest-asyncio", specifier = ">=1.6.0" },
{ name = "openai", specifier = ">=1.100.2" },
{ name = "openai", marker = "extra == 'semantic'", specifier = ">=1.100.2" },
{ name = "pillow", specifier = ">=11.1.0" },
{ name = "psycopg", specifier = "==3.3.1" },
{ name = "pybars3", specifier = ">=0.9.7" },
@@ -237,11 +241,12 @@ requires-dist = [
{ name = "rich", specifier = ">=13.9.4" },
{ name = "sniffio", specifier = ">=1.3.1" },
{ name = "sqlalchemy", specifier = ">=2.0.0" },
{ name = "sqlite-vec", specifier = ">=0.1.6" },
{ name = "sqlite-vec", marker = "extra == 'semantic'", specifier = ">=0.1.6" },
{ name = "typer", specifier = ">=0.9.0" },
{ name = "unidecode", specifier = ">=1.3.8" },
{ name = "watchfiles", specifier = ">=1.0.4" },
]
provides-extras = ["semantic"]
[package.metadata.requires-dev]
dev = [