simlar 1.0.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.
simlar/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ from simlar.contracts import SearchResult, TextIndex, VectorIndex
2
+ from simlar.fusion import ReciprocalRankFusion
3
+ from simlar.indexes.helix_index import HelixIndex
4
+ from simlar.indexes.registry import load_from_directory, register
5
+ from simlar.indexes.relevance_index import RelevanceIndex
6
+ from simlar.indexes.simlar_engine import SimlarEngine
7
+ from simlar.indexes.streaming_index import StreamingHelixIndex as StreamingHybridIndex
8
+
9
+ __all__ = [
10
+ # Contracts — extension points
11
+ "SearchResult",
12
+ "TextIndex",
13
+ "VectorIndex",
14
+ # Fusion
15
+ "ReciprocalRankFusion",
16
+ # Indexes
17
+ "RelevanceIndex",
18
+ "SimlarEngine",
19
+ "HelixIndex",
20
+ "StreamingHybridIndex",
21
+ # Registry
22
+ "register",
23
+ "load_from_directory",
24
+ ]
simlar/contracts.py ADDED
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ import numpy as np
7
+
8
+ # SearchResult and _Parameters are defined in simlar_engine and re-exported here
9
+ # so user code can import them from either package.
10
+ from simlar_engine._types import SearchResult, _Parameters
11
+
12
+ __all__ = [
13
+ "SearchResult",
14
+ "_Parameters",
15
+ "Index",
16
+ "TextIndex",
17
+ "VectorIndex",
18
+ "CompositeIndex",
19
+ "FusionStrategy",
20
+ ]
21
+
22
+ # ── Base ──────────────────────────────────────────────────────────────────────
23
+
24
+
25
+ class Index(ABC):
26
+ """Minimal contract: every index can persist and report its type and size."""
27
+
28
+ @abstractmethod
29
+ def save(self, path: str) -> None: ...
30
+
31
+ @classmethod
32
+ @abstractmethod
33
+ def load(cls, path: str) -> Index: ...
34
+
35
+ @property
36
+ @abstractmethod
37
+ def index_type(self) -> str: ...
38
+
39
+ @property
40
+ @abstractmethod
41
+ def size(self) -> int: ...
42
+
43
+ @property
44
+ @abstractmethod
45
+ def is_trained(self) -> bool: ...
46
+
47
+
48
+ # ── Text index ────────────────────────────────────────────────────────────────
49
+
50
+
51
+ class TextIndex(Index):
52
+ """Index over a text corpus."""
53
+
54
+ # ── Public API ─────────────────────────────────────────────────────────────
55
+
56
+ @abstractmethod
57
+ def add(self, ids: list[str], texts: list[str]) -> None:
58
+ """Append new documents. Raises ValueError on duplicate IDs; use update() to replace."""
59
+
60
+ @abstractmethod
61
+ def update(self, ids: list[str], texts: list[str]) -> None:
62
+ """Replace texts for existing document IDs, then rebuild."""
63
+
64
+ @abstractmethod
65
+ def delete(self, ids: list[str]) -> None:
66
+ """Remove documents by ID, rebuilding internal structures."""
67
+
68
+ @abstractmethod
69
+ def search(self, query: str, k: int) -> list[SearchResult]: ...
70
+
71
+ # ── Internal ───────────────────────────────────────────────────────────────
72
+
73
+ @abstractmethod
74
+ def fit(self, corpus: list[str], parallel: bool = False, **kwargs) -> None:
75
+ """Build the model from a raw corpus list. Called by add() and HelixIndex."""
76
+
77
+ @abstractmethod
78
+ def search_raw(
79
+ self,
80
+ queries: str | list[str],
81
+ k: int,
82
+ parallel: bool = False,
83
+ ) -> tuple[np.ndarray, np.ndarray]:
84
+ """Return (ids, scores) shaped (n_queries, k), int64/float64.
85
+ Used internally by HelixIndex._search_raw().
86
+ """
87
+
88
+
89
+ # ── Vector index ──────────────────────────────────────────────────────────────
90
+
91
+
92
+ class VectorIndex(Index):
93
+ """Index over dense or binary float embeddings."""
94
+
95
+ # ── Public API ─────────────────────────────────────────────────────────────
96
+
97
+ @abstractmethod
98
+ def add(self, ids: list[str], vectors: np.ndarray) -> None: ...
99
+
100
+ @abstractmethod
101
+ def search(self, query: np.ndarray, k: int) -> list[SearchResult]: ...
102
+
103
+ @abstractmethod
104
+ def update(self, ids: list[str], vectors: np.ndarray) -> None:
105
+ """Replace vectors for existing IDs in-place using frozen quantization params."""
106
+
107
+ # ── Internal ───────────────────────────────────────────────────────────────
108
+
109
+ @abstractmethod
110
+ def fit(
111
+ self,
112
+ embeddings: np.ndarray,
113
+ parallel: bool = False,
114
+ params: _Parameters | None = None,
115
+ **kwargs,
116
+ ) -> None:
117
+ """Build from embeddings. Pass params to reuse frozen quantization across shards."""
118
+
119
+ @abstractmethod
120
+ def search_raw(
121
+ self,
122
+ vectors: np.ndarray,
123
+ k: int,
124
+ candidates: np.ndarray | None = None,
125
+ parallel: bool = False,
126
+ n_candidates: int | None = None,
127
+ ) -> tuple[np.ndarray, np.ndarray]:
128
+ """Return (ids, distances) shaped (n_queries, k), int64/float64.
129
+ Used internally by HelixIndex._search_raw().
130
+ """
131
+
132
+ @abstractmethod
133
+ def delete(self, ids: list[str]) -> None:
134
+ """Remove documents by ID, physically rebuilding internal structures."""
135
+
136
+
137
+ # ── Composite index ───────────────────────────────────────────────────────────
138
+
139
+
140
+ class CompositeIndex(Index):
141
+ """Fuses N sub-indexes with a FusionStrategy."""
142
+
143
+ @abstractmethod
144
+ def add(
145
+ self,
146
+ ids: list[str],
147
+ texts: list[str] | None = None,
148
+ vectors: np.ndarray | None = None,
149
+ ) -> None: ...
150
+
151
+ @abstractmethod
152
+ def search(
153
+ self,
154
+ query_text: str | None = None,
155
+ query_vector: np.ndarray | None = None,
156
+ k: int = 10,
157
+ ) -> list[SearchResult]: ...
158
+
159
+ @abstractmethod
160
+ def fit(
161
+ self,
162
+ corpus: list,
163
+ vectors: np.ndarray,
164
+ parallel: bool = False,
165
+ **kwargs,
166
+ ) -> None:
167
+ """Internal: build all sub-indexes. Used by StreamingHybridIndex shards."""
168
+
169
+
170
+ # ── Fusion ────────────────────────────────────────────────────────────────────
171
+
172
+
173
+ @runtime_checkable
174
+ class FusionStrategy(Protocol):
175
+ """Combines N result lists into a single ranked list."""
176
+
177
+ def __call__(
178
+ self,
179
+ results: list[list[SearchResult]],
180
+ k: int,
181
+ ) -> list[SearchResult]: ...
@@ -0,0 +1,3 @@
1
+ from .rrf import ReciprocalRankFusion
2
+
3
+ __all__ = ["ReciprocalRankFusion"]
simlar/fusion/_rrf.pyi ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from simlar.contracts import SearchResult
4
+
5
+ class ReciprocalRankFusion:
6
+ def __init__(self, k: int = 2, weights: list[float] | None = None) -> None: ...
7
+ def __call__(self, results: list[list[SearchResult]], k: int) -> list[SearchResult]: ...
simlar/fusion/rrf.py ADDED
@@ -0,0 +1,3 @@
1
+ from simlar_engine.fusion.rrf import ReciprocalRankFusion
2
+
3
+ __all__ = ["ReciprocalRankFusion"]
@@ -0,0 +1,7 @@
1
+ from .helix_index import HelixIndex
2
+ from .lookup_index import LookupIndex
3
+ from .relevance_index import RelevanceIndex
4
+ from .simlar_engine import SimlarEngine
5
+ from .streaming_index import StreamingHelixIndex
6
+
7
+ __all__ = ["LookupIndex", "RelevanceIndex", "SimlarEngine", "StreamingHelixIndex", "HelixIndex"]
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from simlar.contracts import (
6
+ CompositeIndex,
7
+ FusionStrategy,
8
+ SearchResult,
9
+ TextIndex,
10
+ VectorIndex,
11
+ _Parameters,
12
+ )
13
+
14
+ class HelixIndex(CompositeIndex):
15
+ """
16
+ Example::
17
+
18
+ from simlar import HelixIndex, RelevanceIndex, SimlarEngine
19
+ from simlar.fusion import ReciprocalRankFusion
20
+
21
+ idx = HelixIndex(
22
+ text_index=RelevanceIndex(),
23
+ vector_index=SimlarEngine(),
24
+ fusion=ReciprocalRankFusion(),
25
+ )
26
+ idx.fit(corpus=texts, vectors=embeddings)
27
+ idx.add(ids=ids, texts=texts, vectors=embeddings)
28
+ results = idx.search(query_text="query", query_vector=q_vec, k=10)
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ text_index: TextIndex | None = None,
34
+ vector_index: VectorIndex | None = None,
35
+ fusion: FusionStrategy | None = None,
36
+ text_k: int = 5000,
37
+ vector_k: int = 1000,
38
+ top_k: int = 100,
39
+ alpha_text: float = 0.10,
40
+ alpha_vector: float = 1.0,
41
+ rrf_k: int = 2,
42
+ ) -> None: ...
43
+ def fit(
44
+ self,
45
+ corpus: list[str],
46
+ vectors: np.ndarray,
47
+ parallel: bool = False,
48
+ params: _Parameters | None = None,
49
+ **kwargs: object,
50
+ ) -> None: ...
51
+ def add(
52
+ self,
53
+ ids: list[str],
54
+ texts: list[str] | None = None,
55
+ vectors: np.ndarray | None = None,
56
+ ) -> None: ...
57
+ def search(
58
+ self,
59
+ query_text: str | list[str] | None = None,
60
+ query_vector: np.ndarray | None = None,
61
+ k: int | None = None,
62
+ parallel: bool = False,
63
+ ) -> list[SearchResult]: ...
64
+ def save(self, directory: str) -> None: ...
65
+ @classmethod
66
+ def load(cls, directory: str) -> HelixIndex: ...
67
+ @property
68
+ def size(self) -> int: ...
69
+ @property
70
+ def is_trained(self) -> bool: ...
71
+ @property
72
+ def index_type(self) -> str: ...
73
+ @property
74
+ def text_index(self) -> TextIndex: ...
75
+ @property
76
+ def vector_index(self) -> VectorIndex: ...
77
+ @property
78
+ def boundaries(self) -> np.ndarray | None: ...
79
+ @property
80
+ def fit_values(self) -> np.ndarray | None: ...
81
+ @property
82
+ def quantization_params(self) -> _Parameters | None: ...
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from simlar.contracts import SearchResult, TextIndex
6
+
7
+ class RelevanceIndex(TextIndex):
8
+ """
9
+
10
+ Example::
11
+
12
+ idx = RelevanceIndex(k1=1.5, b=0.75)
13
+ idx.add(ids=["a", "b"], texts=["first doc", "second doc"])
14
+ results = idx.search("first", k=10)
15
+ idx.save("bm25.idx")
16
+ idx = RelevanceIndex.load("idx")
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ method: str = "robertson",
22
+ k1: float = 1.5,
23
+ b: float = 0.75,
24
+ stopwords_lang: str = "english",
25
+ stemmer_lang: str = "english",
26
+ ) -> None: ...
27
+ def fit(self, corpus: list[str], parallel: bool = False, **kwargs: object) -> None: ...
28
+ def add(self, ids: list[str], texts: list[str]) -> None: ...
29
+ def update(self, ids: list[str], texts: list[str]) -> None: ...
30
+ def delete(self, ids: list[str]) -> None: ...
31
+ def search(self, query: str, k: int) -> list[SearchResult]: ...
32
+ def search_raw(
33
+ self,
34
+ queries: str | list[str],
35
+ k: int,
36
+ parallel: bool = False,
37
+ ) -> tuple[np.ndarray, np.ndarray]: ...
38
+ def save(self, directory: str) -> None: ...
39
+ @classmethod
40
+ def load(cls, directory: str) -> RelevanceIndex: ...
41
+ @property
42
+ def size(self) -> int: ...
43
+ @property
44
+ def is_trained(self) -> bool: ...
45
+ @property
46
+ def index_type(self) -> str: ...
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from simlar.contracts import SearchResult, VectorIndex, _Parameters
6
+
7
+ class SimlarEngine(VectorIndex):
8
+ """
9
+ Example::
10
+
11
+ idx = SimlarEngine()
12
+ idx.fit(embeddings=vecs)
13
+ idx.add(ids=["a", "b"], vectors=vecs)
14
+ results = idx.search(query=q_vec, k=10)
15
+ """
16
+
17
+ def __init__(self) -> None: ...
18
+ def fit(
19
+ self,
20
+ embeddings: np.ndarray,
21
+ parallel: bool = False,
22
+ params: _Parameters | None = None,
23
+ **kwargs: object,
24
+ ) -> None: ...
25
+ def add(self, ids: list[str], vectors: np.ndarray) -> None: ...
26
+ def update(self, ids: list[str], vectors: np.ndarray) -> None: ...
27
+ def delete(self, ids: list[str]) -> None: ...
28
+ def search(self, query: np.ndarray, k: int) -> list[SearchResult]: ...
29
+ def search_raw(
30
+ self,
31
+ vectors: np.ndarray,
32
+ k: int,
33
+ candidates: np.ndarray | None = None,
34
+ parallel: bool = False,
35
+ n_candidates: int | None = None,
36
+ ) -> tuple[np.ndarray, np.ndarray]: ...
37
+ def update_vector(self, doc_id: int, vector: np.ndarray) -> None: ...
38
+ def save(self, directory: str) -> None: ...
39
+ @classmethod
40
+ def load(cls, directory: str) -> SimlarEngine: ...
41
+ @property
42
+ def size(self) -> int: ...
43
+ @property
44
+ def is_trained(self) -> bool: ...
45
+ @property
46
+ def index_type(self) -> str: ...
47
+ @property
48
+ def boundaries(self) -> np.ndarray | None: ...
49
+ @property
50
+ def fit_values(self) -> np.ndarray | None: ...
51
+ @property
52
+ def _params(self) -> _Parameters | None: ...
53
+ @property
54
+ def _matrix(self) -> np.ndarray | None: ...
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from simlar.contracts import TextIndex, VectorIndex, _Parameters
6
+
7
+ class StreamingHybridIndex:
8
+ """
9
+ Example::
10
+
11
+ idx = StreamingHybridIndex()
12
+ idx.add_batch(corpus=texts[:1000], vectors=vecs[:1000])
13
+ idx.add_batch(corpus=texts[1000:], vectors=vecs[1000:])
14
+ ids, distances = idx.search(query_text="query", query_vector=q_vec, k=10)
15
+ idx.save("streaming.idx")
16
+ idx = StreamingHybridIndex.load("streaming.idx")
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ text_index_cls: type[TextIndex] | None = None,
22
+ vector_index_cls: type[VectorIndex] | None = None,
23
+ text_k: int = 5000,
24
+ vector_k: int = 1000,
25
+ top_k: int = 100,
26
+ alpha_text: float = 0.10,
27
+ alpha_vector: float = 1.0,
28
+ rrf_k: int = 2,
29
+ n_candidates: int = 5000,
30
+ ) -> None: ...
31
+ def add_batch(self, corpus: list[str], vectors: np.ndarray, parallel: bool = False) -> None: ...
32
+ async def add_batch_async(
33
+ self, corpus: list[str], vectors: np.ndarray, parallel: bool = False
34
+ ) -> None: ...
35
+ def search(
36
+ self,
37
+ query_text: str | list[str],
38
+ query_vector: np.ndarray,
39
+ k: int | None = None,
40
+ parallel: bool = False,
41
+ ) -> tuple[np.ndarray, np.ndarray]: ...
42
+ def fit(self, corpus: list, vectors: np.ndarray, **kwargs: object) -> None: ...
43
+ def save(self, directory: str) -> None: ...
44
+ @classmethod
45
+ def load(cls, directory: str) -> StreamingHybridIndex: ...
46
+ @property
47
+ def size(self) -> int: ...
48
+ @property
49
+ def n_shards(self) -> int: ...
50
+ @property
51
+ def is_trained(self) -> bool: ...
52
+ @property
53
+ def index_type(self) -> str: ...
54
+ @property
55
+ def boundaries(self) -> np.ndarray | None: ...
56
+ @property
57
+ def fit_values(self) -> np.ndarray | None: ...
58
+ @property
59
+ def _params(self) -> _Parameters | None: ...
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from simlar_engine.indexes._helix_impl import _HelixCore
5
+
6
+ from simlar.contracts import (
7
+ CompositeIndex,
8
+ FusionStrategy,
9
+ SearchResult,
10
+ TextIndex,
11
+ VectorIndex,
12
+ _Parameters,
13
+ )
14
+ from simlar.indexes.registry import register
15
+
16
+
17
+ @register("helix")
18
+ class HelixIndex(CompositeIndex):
19
+ """Fuses N indexes with a FusionStrategy.
20
+
21
+ Example::
22
+
23
+ HelixIndex(indexes=[RelevanceIndex(), SimilarityIndex()], fusion=ReciprocalRankFusion())
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ text_index: TextIndex | None = None,
30
+ vector_index: VectorIndex | None = None,
31
+ fusion: FusionStrategy | None = None,
32
+ text_k: int | None = None,
33
+ vector_k: int | None = None,
34
+ top_k: int = 100,
35
+ alpha_text: float = 0.10,
36
+ alpha_vector: float = 1.0,
37
+ ) -> None:
38
+ self._core = _HelixCore(
39
+ text_index=text_index,
40
+ vector_index=vector_index,
41
+ fusion=fusion,
42
+ text_k=text_k,
43
+ vector_k=vector_k,
44
+ top_k=top_k,
45
+ alpha_text=alpha_text,
46
+ alpha_vector=alpha_vector,
47
+ )
48
+
49
+ # ── Public API ────────────────────────────────────────────────────────────
50
+
51
+ def add(
52
+ self,
53
+ ids: list[str],
54
+ texts: list[str] | None = None,
55
+ vectors: np.ndarray | None = None,
56
+ ) -> None:
57
+ self._core.add(ids, texts, vectors)
58
+
59
+ def search(
60
+ self,
61
+ query_text: str | list[str] | None = None,
62
+ query_vector: np.ndarray | None = None,
63
+ k: int | None = None,
64
+ parallel: bool = False,
65
+ ) -> list[SearchResult] | list[list[SearchResult]]:
66
+ return self._core.search(query_text, query_vector, k, parallel)
67
+
68
+ def fit(
69
+ self,
70
+ corpus: list,
71
+ vectors: np.ndarray,
72
+ parallel: bool = False,
73
+ **kwargs,
74
+ ) -> None:
75
+ params = kwargs.pop("params", None)
76
+ self._core.fit(corpus, vectors, parallel, params)
77
+
78
+ def save(self, directory: str) -> None:
79
+ self._core.save(directory)
80
+
81
+ @classmethod
82
+ def load(cls, directory: str) -> HelixIndex:
83
+ obj = cls.__new__(cls)
84
+ obj._core = _HelixCore.load(directory)
85
+ return obj
86
+
87
+ # ── Metadata ──────────────────────────────────────────────────────────────
88
+
89
+ @property
90
+ def size(self) -> int:
91
+ return self._core.size
92
+
93
+ @property
94
+ def is_trained(self) -> bool:
95
+ return self._core.is_trained
96
+
97
+ @property
98
+ def index_type(self) -> str:
99
+ return "helix"
100
+
101
+ @property
102
+ def boundaries(self) -> np.ndarray | None:
103
+ return self._core.boundaries
104
+
105
+ @property
106
+ def fit_values(self) -> np.ndarray | None:
107
+ return self._core.fit_values
108
+
109
+ @property
110
+ def _params(self) -> _Parameters | None:
111
+ return self._core._params
112
+
113
+ @property
114
+ def text_index(self) -> TextIndex:
115
+ return self._core.text_index
116
+
117
+ @property
118
+ def vector_index(self) -> VectorIndex:
119
+ return self._core.vector_index
120
+
121
+ def __repr__(self) -> str:
122
+ return (
123
+ f"HelixIndex(text={self._core.text_index.index_type!r}, "
124
+ f"vector={self._core.vector_index.index_type!r}, "
125
+ f"text_k={self._core._text_k}, vector_k={self._core._vector_k})"
126
+ )
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from simlar_engine.indexes._lookup_impl import _TextCore
5
+
6
+ from simlar.contracts import SearchResult, TextIndex
7
+ from simlar.indexes.registry import register
8
+
9
+
10
+ @register("lookup")
11
+ class LookupIndex(TextIndex):
12
+ """
13
+ Example:
14
+ >>> idx = LookupIndex()
15
+ >>> idx.add(["a", "b"], ["hello world", "foo bar"])
16
+ >>> results = idx.search("hello", k=5)
17
+ >>> results[0].id
18
+ 'a'
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ stopwords_lang: str = "english",
24
+ stemmer_lang: str = "english",
25
+ ) -> None:
26
+ self._core = _TextCore(stopwords_lang, stemmer_lang)
27
+
28
+ # ── Public contract ───────────────────────────────────────────────────────
29
+
30
+ def fit(self, corpus: list[str], parallel: bool = False, **kwargs: object) -> None:
31
+ self._core.fit(corpus, parallel)
32
+
33
+ def add(self, ids: list[str], texts: list[str]) -> None:
34
+ self._core.add(ids, texts)
35
+
36
+ def update(self, ids: list[str], texts: list[str]) -> None:
37
+ self._core.update(ids, texts)
38
+
39
+ def delete(self, ids: list[str]) -> None:
40
+ self._core.delete(ids)
41
+
42
+ def search(self, query: str, k: int = 10) -> list[SearchResult]:
43
+ return self._core.search(query, k)
44
+
45
+ def search_raw(
46
+ self,
47
+ queries: str | list[str],
48
+ k: int,
49
+ parallel: bool = False,
50
+ ) -> tuple[np.ndarray, np.ndarray]:
51
+ return self._core.search_raw(queries, k, parallel)
52
+
53
+ def save(self, directory: str) -> None:
54
+ self._core.save(directory)
55
+
56
+ @classmethod
57
+ def load(cls, directory: str) -> LookupIndex:
58
+ obj = cls.__new__(cls)
59
+ obj._core = _TextCore.load(directory)
60
+ return obj
61
+
62
+ # ── Properties ────────────────────────────────────────────────────────────
63
+
64
+ @property
65
+ def size(self) -> int:
66
+ return self._core.size
67
+
68
+ @property
69
+ def is_trained(self) -> bool:
70
+ return self._core.is_trained
71
+
72
+ @property
73
+ def index_type(self) -> str:
74
+ return self._core.index_type
75
+
76
+ @property
77
+ def ids(self) -> list[str]:
78
+ return self._core.ids