markdown-memory-vec 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: markdown-memory-vec
3
+ Version: 0.1.0
4
+ Summary: Lightweight vector search for Markdown-based memory systems. Chunk, embed, index, and hybrid-search your .md knowledge base with sqlite-vec.
5
+ Project-URL: Homepage, https://github.com/aigente/markdown-memory-vec
6
+ Project-URL: Repository, https://github.com/aigente/markdown-memory-vec
7
+ Author-email: Aigente <dev@aigente.io>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: RAG,embeddings,markdown,memory,semantic-search,sqlite-vec,vector-search
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Text Processing :: Indexing
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pyyaml>=6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
25
+ Requires-Dist: pyright>=1.1.380; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
28
+ Requires-Dist: types-pyyaml; extra == 'dev'
29
+ Provides-Extra: vector
30
+ Requires-Dist: sentence-transformers>=3.0; extra == 'vector'
31
+ Requires-Dist: sqlite-vec>=0.1.6; extra == 'vector'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # markdown-memory-vec
35
+
36
+ Lightweight vector search for Markdown-based memory systems. Chunk, embed, index, and hybrid-search your `.md` knowledge base with sqlite-vec.
37
+
38
+ <!-- Badges -->
39
+ <!-- [![PyPI version](https://badge.fury.io/py/markdown-memory-vec.svg)](https://pypi.org/project/markdown-memory-vec/) -->
40
+ <!-- [![Python](https://img.shields.io/pypi/pyversions/markdown-memory-vec.svg)](https://pypi.org/project/markdown-memory-vec/) -->
41
+ <!-- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -->
42
+
43
+ ## Features
44
+
45
+ - **Markdown-native**: YAML frontmatter parsing for metadata (importance, type, tags)
46
+ - **Smart chunking**: Paragraph-aware splitting with configurable overlap (~400 tokens, 80 overlap)
47
+ - **SHA-256 dedup**: Never re-embed unchanged content — incremental indexing is fast
48
+ - **Hybrid search**: Combines semantic similarity (α), importance weighting (β), and temporal decay (γ)
49
+ - **Zero-copy storage**: sqlite-vec KNN search with cosine distance in a single `.db` file
50
+
51
+ ## Quick Start
52
+
53
+ ### Installation
54
+
55
+ ```bash
56
+ # Core only (YAML parsing, chunking, interfaces)
57
+ pip install markdown-memory-vec
58
+
59
+ # With vector search support (sqlite-vec + sentence-transformers)
60
+ pip install 'markdown-memory-vec[vector]'
61
+ ```
62
+
63
+ ### Python API
64
+
65
+ ```python
66
+ from memory_vec import MemoryVectorService
67
+
68
+ svc = MemoryVectorService("/path/to/project")
69
+ svc.rebuild_index() # Full index build
70
+ results = svc.search("how to deploy") # Hybrid search
71
+ svc.close()
72
+ ```
73
+
74
+ ### Low-level API
75
+
76
+ ```python
77
+ from memory_vec import (
78
+ SqliteVecStore,
79
+ SentenceTransformerEmbedder,
80
+ MemoryIndexer,
81
+ HybridSearchService,
82
+ )
83
+
84
+ store = SqliteVecStore("memory.db")
85
+ store.ensure_tables()
86
+
87
+ embedder = SentenceTransformerEmbedder()
88
+ indexer = MemoryIndexer(store, embedder, memory_root="/path/to/memory")
89
+ indexer.index_directory("/path/to/memory")
90
+
91
+ search = HybridSearchService(vec_store=store, embedder=embedder)
92
+ results = search.search("how to deploy")
93
+ for r in results:
94
+ print(f"{r.file_path} (score={r.hybrid_score:.3f}): {r.chunk_text[:80]}...")
95
+ ```
96
+
97
+ ## CLI Usage
98
+
99
+ ```bash
100
+ # Full rebuild
101
+ memory-vec /path/to/project --rebuild
102
+
103
+ # Incremental update (only changed files)
104
+ memory-vec /path/to/project --incremental
105
+
106
+ # Search
107
+ memory-vec /path/to/project --search "how to deploy" --top-k 5
108
+
109
+ # Statistics
110
+ memory-vec /path/to/project --stats
111
+
112
+ # Custom memory subdirectory
113
+ memory-vec /path/to/project --rebuild --memory-subdir "docs/memory"
114
+
115
+ # Verbose logging
116
+ memory-vec /path/to/project --rebuild -v
117
+ ```
118
+
119
+ ## API Reference
120
+
121
+ ### High-level
122
+
123
+ | Class | Description |
124
+ |-------|-------------|
125
+ | `MemoryVectorService` | All-in-one service: rebuild, incremental index, search, stats |
126
+
127
+ ### Components
128
+
129
+ | Class | Description |
130
+ |-------|-------------|
131
+ | `SqliteVecStore` | sqlite-vec backed vector store with KNN search |
132
+ | `SentenceTransformerEmbedder` | Lazy-loading sentence-transformers embedder |
133
+ | `MemoryIndexer` | Markdown → chunks → embeddings → store pipeline |
134
+ | `HybridSearchService` | Hybrid scoring: `α×semantic + β×importance + γ×temporal` |
135
+
136
+ ### Interfaces
137
+
138
+ | Interface | Description |
139
+ |-----------|-------------|
140
+ | `IEmbedder` | Abstract embedder (`embed`, `embed_batch`, `dimension`) |
141
+ | `ISqliteVecStore` | Abstract vector store (`add`, `search`, `delete`, `clear`, `count`) |
142
+
143
+ ### Data Types
144
+
145
+ | Type | Description |
146
+ |------|-------------|
147
+ | `VectorRecord` | Record for insertion (id, embedding, metadata) |
148
+ | `VectorSearchResult` | Raw KNN result (id, distance, metadata) |
149
+ | `SearchResult` | Hybrid search result with all score components |
150
+ | `MemoryVecMeta` | Metadata dataclass for stored embeddings |
151
+
152
+ ### Utilities
153
+
154
+ | Function | Description |
155
+ |----------|-------------|
156
+ | `chunk_text(text, chunk_size, overlap_size)` | Split text into overlapping chunks |
157
+ | `parse_frontmatter(text)` | Extract YAML frontmatter from Markdown |
158
+ | `content_hash(text)` | SHA-256 hex digest |
159
+ | `is_sqlite_vec_available()` | Check sqlite-vec availability |
160
+ | `is_sentence_transformers_available()` | Check sentence-transformers availability |
161
+
162
+ ## Architecture
163
+
164
+ ```
165
+ ┌─────────────────────────────────────────────────────┐
166
+ │ MemoryVectorService │
167
+ │ (high-level orchestration layer) │
168
+ ├──────────┬──────────┬──────────────┬────────────────┤
169
+ │ │ │ │ │
170
+ │ Indexer │ Search │ Embedder │ Store │
171
+ │ │ │ │ │
172
+ │ .md file │ hybrid │ sentence- │ sqlite-vec │
173
+ │ → chunks │ scoring │ transformers│ KNN + meta │
174
+ │ → embed │ α+β+γ │ (lazy load) │ (cosine) │
175
+ │ → store │ │ │ │
176
+ └──────────┴──────────┴──────────────┴────────────────┘
177
+ ▲ │
178
+ │ YAML frontmatter │
179
+ │ importance/type/tags ▼
180
+ ┌─┴─────────────────────┐ ┌─────────────────────┐
181
+ │ Markdown Files │ │ vector_index.db │
182
+ │ (.claude/memory/) │ │ (single file) │
183
+ └───────────────────────┘ └─────────────────────┘
184
+ ```
185
+
186
+ ## Configuration
187
+
188
+ ### HuggingFace Model
189
+
190
+ By default, uses `paraphrase-multilingual-MiniLM-L12-v2` (384-dim, 50+ languages).
191
+
192
+ For users in China or regions with slow HuggingFace access:
193
+
194
+ ```bash
195
+ # Use a mirror
196
+ export HF_ENDPOINT=https://hf-mirror.com
197
+
198
+ # Or use offline mode (model must be pre-cached)
199
+ export HF_HUB_OFFLINE=1
200
+ ```
201
+
202
+ ### Hybrid Search Weights
203
+
204
+ Default: `α=0.6, β=0.2, γ=0.2, λ=0.05`
205
+
206
+ ```python
207
+ search = HybridSearchService(
208
+ vec_store=store,
209
+ embedder=embedder,
210
+ alpha=0.8, # Semantic weight
211
+ beta=0.1, # Importance weight
212
+ gamma=0.1, # Temporal decay weight
213
+ decay_lambda=0.03, # Slower decay
214
+ )
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,13 @@
1
+ memory_vec/__init__.py,sha256=TYFh7VgOBBw2A1GjM2uTuxr_wIoVASjcjaI9eiddvLw,2356
2
+ memory_vec/__main__.py,sha256=_HGhIzBVmsnb1XKqV_VOGMdIVlPrOWFDNhB3PLFNvG0,3978
3
+ memory_vec/embedder.py,sha256=dPn0DGnNSQ8Uj_gbDDq2FlC8hbRTvOQUBHxWOtEeN3w,5260
4
+ memory_vec/indexer.py,sha256=ZBE7KtY-2x77x5p89SpiZ3oKKqLs2LJGE9SkAu1vTgI,10701
5
+ memory_vec/interfaces.py,sha256=A_vyOIlMiBO3UkW4AMCtpq58pGbSASiIHhAiSvfm16c,2810
6
+ memory_vec/search.py,sha256=aEkfHjJeeUtaLUksfzAW2UfJsEPOoHcnBnVfzXgoQ1w,8624
7
+ memory_vec/service.py,sha256=AylPef106ZWLB9NiEr2Uee2LB0IZjrYiEs8HYn8TtNU,11533
8
+ memory_vec/store.py,sha256=G2GJYtcqYA0Op4oOgjIRZLDzTmpI8HANoUqEMrOwO88,17319
9
+ markdown_memory_vec-0.1.0.dist-info/METADATA,sha256=Xob56dfeNQX5M81IALmxX8uEQioScFS3Y19m-JZCJWE,7956
10
+ markdown_memory_vec-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ markdown_memory_vec-0.1.0.dist-info/entry_points.txt,sha256=TGn2LA56FfN5WnjyVjbe-Q7GqW7NMU2VvbEMXeEHYN8,56
12
+ markdown_memory_vec-0.1.0.dist-info/licenses/LICENSE,sha256=GeX_PyX3s1Hao9OpOj1gon8E4w_75fcZMqvhdA5bd5c,1064
13
+ markdown_memory_vec-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memory-vec = memory_vec.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aigente
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
memory_vec/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ Vector search infrastructure for Markdown-based memory systems.
3
+
4
+ This package provides:
5
+
6
+ - **Interfaces** (``interfaces.py``): ``IEmbedder``, ``ISqliteVecStore``
7
+ - **Storage** (``store.py``): ``SqliteVecStore`` — concrete sqlite-vec
8
+ backed vector store implementing ``ISqliteVecStore``
9
+ - **Embedder** (``embedder.py``): ``SentenceTransformerEmbedder`` — concrete
10
+ embedder using sentence-transformers, implementing ``IEmbedder``
11
+ - **Indexer** (``indexer.py``): ``MemoryIndexer`` — Markdown-to-vector indexing
12
+ pipeline with chunking, SHA-256 dedup, and YAML frontmatter parsing
13
+ - **Search** (``search.py``): ``HybridSearchService`` — hybrid retrieval
14
+ combining semantic similarity, importance, and temporal decay
15
+
16
+ All heavy dependencies (``sqlite-vec``, ``sentence-transformers``) are optional.
17
+ Use the ``is_*_available()`` helpers to check at runtime.
18
+
19
+ Quick start::
20
+
21
+ from memory_vec import (
22
+ SqliteVecStore,
23
+ SentenceTransformerEmbedder,
24
+ MemoryIndexer,
25
+ HybridSearchService,
26
+ )
27
+
28
+ store = SqliteVecStore("memory.db")
29
+ store.ensure_tables()
30
+
31
+ embedder = SentenceTransformerEmbedder()
32
+ indexer = MemoryIndexer(store, embedder)
33
+ indexer.index_directory("path/to/markdown/files")
34
+
35
+ search = HybridSearchService(vec_store=store, embedder=embedder)
36
+ results = search.search("how to deploy")
37
+ """
38
+
39
+ # Interfaces
40
+ # Concrete implementations
41
+ from .embedder import SentenceTransformerEmbedder, is_sentence_transformers_available
42
+ from .indexer import MemoryIndexer, chunk_text, parse_frontmatter
43
+ from .interfaces import IEmbedder, ISqliteVecStore, VectorRecord, VectorSearchResult
44
+ from .search import HybridSearchService, SearchResult
45
+ from .service import MemoryVectorService
46
+ from .store import MemoryVecMeta, SqliteVecStore, content_hash, is_sqlite_vec_available
47
+
48
+ __all__ = [
49
+ # Interfaces
50
+ "IEmbedder",
51
+ "ISqliteVecStore",
52
+ "VectorRecord",
53
+ "VectorSearchResult",
54
+ # Store
55
+ "SqliteVecStore",
56
+ "MemoryVecMeta",
57
+ "content_hash",
58
+ "is_sqlite_vec_available",
59
+ # Embedder
60
+ "SentenceTransformerEmbedder",
61
+ "is_sentence_transformers_available",
62
+ # Indexer
63
+ "MemoryIndexer",
64
+ "chunk_text",
65
+ "parse_frontmatter",
66
+ # Search
67
+ "HybridSearchService",
68
+ "SearchResult",
69
+ # High-level service
70
+ "MemoryVectorService",
71
+ ]
72
+
73
+ __version__ = "0.1.0"
memory_vec/__main__.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ CLI entry point: ``python -m memory_vec`` or ``memory-vec``.
3
+
4
+ Provides commands for building, maintaining, and querying the vector index.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import logging
11
+ import sys
12
+
13
+ from .service import MemoryVectorService
14
+
15
+
16
+ def main() -> None:
17
+ """Command-line interface for memory vector maintenance."""
18
+ parser = argparse.ArgumentParser(
19
+ prog="memory-vec",
20
+ description="Markdown memory vector index — build, maintain, and search",
21
+ formatter_class=argparse.RawDescriptionHelpFormatter,
22
+ epilog=(
23
+ "Examples:\n"
24
+ " memory-vec /path/to/project --rebuild\n"
25
+ " memory-vec /path/to/project --incremental\n"
26
+ " memory-vec /path/to/project --search 'how to deploy'\n"
27
+ " memory-vec /path/to/project --stats\n"
28
+ "\n"
29
+ "Environment variables:\n"
30
+ " HF_ENDPOINT HuggingFace mirror URL (e.g. https://hf-mirror.com)\n"
31
+ " HF_HUB_OFFLINE Set to '1' to disable network access\n"
32
+ ),
33
+ )
34
+ parser.add_argument("workspace", help="Project root directory")
35
+ parser.add_argument("--rebuild", action="store_true", help="Full rebuild of vector index")
36
+ parser.add_argument("--incremental", action="store_true", help="Incremental re-index (only changed files)")
37
+ parser.add_argument("--search", type=str, help="Search memories by query")
38
+ parser.add_argument("--stats", action="store_true", help="Show index statistics")
39
+ parser.add_argument("--top-k", type=int, default=5, help="Number of search results (default: 5)")
40
+ parser.add_argument(
41
+ "--model",
42
+ type=str,
43
+ default="paraphrase-multilingual-MiniLM-L12-v2",
44
+ help="Embedding model name",
45
+ )
46
+ parser.add_argument(
47
+ "--memory-subdir",
48
+ type=str,
49
+ default=".claude/memory",
50
+ help="Memory subdirectory relative to workspace (default: .claude/memory)",
51
+ )
52
+ parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
53
+
54
+ args = parser.parse_args()
55
+
56
+ logging.basicConfig(
57
+ level=logging.DEBUG if args.verbose else logging.INFO,
58
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
59
+ )
60
+
61
+ svc = MemoryVectorService(
62
+ args.workspace,
63
+ model_name=args.model,
64
+ memory_subdir=args.memory_subdir,
65
+ )
66
+
67
+ if not svc.is_available:
68
+ print("ERROR: Vector dependencies not installed.", file=sys.stderr) # noqa: T201
69
+ print("Install with: pip install 'markdown-memory-vec[vector]'", file=sys.stderr) # noqa: T201
70
+ sys.exit(1)
71
+
72
+ try:
73
+ if args.rebuild:
74
+ total = svc.rebuild_index()
75
+ print(f"Full rebuild complete: {total} chunks indexed") # noqa: T201
76
+
77
+ elif args.incremental:
78
+ total = svc.incremental_index()
79
+ print(f"Incremental index: {total} chunks updated") # noqa: T201
80
+
81
+ elif args.search:
82
+ results = svc.search(args.search, top_k=args.top_k)
83
+ if not results:
84
+ print("No results found.") # noqa: T201
85
+ else:
86
+ for i, r in enumerate(results, 1):
87
+ print(f"\n--- Result {i} (score: {r['hybrid_score']}) ---") # noqa: T201
88
+ print(f"File: {r['file_path']}") # noqa: T201
89
+ print(f"Type: {r['memory_type']} Importance: {r['importance']}") # noqa: T201
90
+ text = str(r["chunk_text"])
91
+ if len(text) > 200:
92
+ text = text[:200] + "..."
93
+ print(f"Text: {text}") # noqa: T201
94
+
95
+ elif args.stats:
96
+ s = svc.stats()
97
+ print("Vector Index Statistics:") # noqa: T201
98
+ for k, v in s.items():
99
+ print(f" {k}: {v}") # noqa: T201
100
+
101
+ else:
102
+ parser.print_help()
103
+
104
+ finally:
105
+ svc.close()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
memory_vec/embedder.py ADDED
@@ -0,0 +1,137 @@
1
+ """
2
+ Embedding model abstraction layer.
3
+
4
+ Provides a concrete implementation :class:`SentenceTransformerEmbedder` that
5
+ wraps the ``sentence-transformers`` library and conforms to the
6
+ :class:`IEmbedder` interface defined in ``interfaces.py``.
7
+
8
+ The heavy ML import is deferred until the first call so that importing this
9
+ module is always cheap.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Any, List
18
+
19
+ from .interfaces import IEmbedder
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Availability check
25
+ # ---------------------------------------------------------------------------
26
+ _sentence_transformers_available = False
27
+ try:
28
+ import sentence_transformers # noqa: F401 # type: ignore[import-untyped]
29
+
30
+ _sentence_transformers_available = True
31
+ except ImportError:
32
+ pass
33
+
34
+
35
+ def is_sentence_transformers_available() -> bool:
36
+ """Return ``True`` if the sentence-transformers package is importable."""
37
+ return _sentence_transformers_available
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Sentence-transformers implementation
42
+ # ---------------------------------------------------------------------------
43
+ _DEFAULT_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
44
+ _DEFAULT_DIMENSION = 384
45
+
46
+
47
+ def _ensure_hf_env(model_name: str) -> None:
48
+ """Set HuggingFace environment variables for reliable model loading.
49
+
50
+ Strategy:
51
+ - If model is already cached locally → set ``HF_HUB_OFFLINE=1`` (no network).
52
+ - Otherwise respect user-set ``HF_ENDPOINT`` (do **not** override it).
53
+ """
54
+ if os.environ.get("HF_HUB_OFFLINE") == "1":
55
+ return # User explicitly requested offline mode
56
+
57
+ # Check HuggingFace hub cache for the model
58
+ cache_dir = Path.home() / ".cache" / "huggingface" / "hub"
59
+ model_dir = cache_dir / f"models--sentence-transformers--{model_name}"
60
+ if model_dir.exists():
61
+ os.environ["HF_HUB_OFFLINE"] = "1"
62
+ logger.info("Model '%s' found in local cache, using offline mode", model_name)
63
+ return
64
+
65
+ # Model not cached — log a hint if no endpoint is set
66
+ if not os.environ.get("HF_ENDPOINT"):
67
+ logger.info(
68
+ "HF_ENDPOINT not set. If downloading is slow, set HF_ENDPOINT to a mirror "
69
+ "(e.g. https://hf-mirror.com for users in China)."
70
+ )
71
+
72
+
73
+ class SentenceTransformerEmbedder(IEmbedder):
74
+ """Embedder backed by the ``sentence-transformers`` library.
75
+
76
+ The underlying ``SentenceTransformer`` model is loaded lazily on the first
77
+ call to :meth:`embed` or :meth:`embed_batch`, and is then cached as a
78
+ class-level variable so it is shared across all instances using the same
79
+ model name.
80
+
81
+ Parameters
82
+ ----------
83
+ model_name:
84
+ HuggingFace model identifier. Defaults to
85
+ ``"paraphrase-multilingual-MiniLM-L12-v2"`` (384-dim, 50+ languages).
86
+ """
87
+
88
+ # Class-level cache: model_name -> SentenceTransformer instance
89
+ _model_cache: dict[str, Any] = {}
90
+
91
+ def __init__(self, model_name: str = _DEFAULT_MODEL_NAME) -> None:
92
+ if not _sentence_transformers_available:
93
+ raise RuntimeError(
94
+ "sentence-transformers is not installed. " "Install it with: pip install 'markdown-memory-vec[vector]'"
95
+ )
96
+ self._model_name = model_name
97
+ self._dimension_override: int | None = None
98
+
99
+ # -- lazy model loading --------------------------------------------------
100
+
101
+ @property
102
+ def _model(self) -> Any:
103
+ """Return the cached ``SentenceTransformer`` instance."""
104
+ if self._model_name not in self._model_cache:
105
+ from sentence_transformers import SentenceTransformer # type: ignore[import-untyped]
106
+
107
+ _ensure_hf_env(self._model_name)
108
+ logger.info("Loading sentence-transformer model: %s", self._model_name)
109
+ model = SentenceTransformer(self._model_name)
110
+ self._model_cache[self._model_name] = model
111
+ # Infer dimension from the model
112
+ dim: int = model.get_sentence_embedding_dimension() # type: ignore[assignment]
113
+ self._dimension_override = dim
114
+ return self._model_cache[self._model_name]
115
+
116
+ # -- IEmbedder interface implementation ----------------------------------
117
+
118
+ def embed(self, text: str) -> List[float]:
119
+ """Embed a single text string into a vector."""
120
+ result = self._model.encode([text], show_progress_bar=False)
121
+ return result[0].tolist() # type: ignore[union-attr, no-any-return]
122
+
123
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
124
+ """Embed multiple texts into vectors."""
125
+ if not texts:
126
+ return []
127
+ embeddings = self._model.encode(texts, show_progress_bar=False)
128
+ return [vec.tolist() for vec in embeddings] # type: ignore[union-attr]
129
+
130
+ @property
131
+ def dimension(self) -> int:
132
+ """Return the vector dimension (triggers model load if unknown)."""
133
+ if self._dimension_override is not None:
134
+ return self._dimension_override
135
+ # Trigger model load to discover dimension
136
+ _ = self._model
137
+ return self._dimension_override or _DEFAULT_DIMENSION