mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user