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 +24 -0
- simlar/contracts.py +181 -0
- simlar/fusion/__init__.py +3 -0
- simlar/fusion/_rrf.pyi +7 -0
- simlar/fusion/rrf.py +3 -0
- simlar/indexes/__init__.py +7 -0
- simlar/indexes/_helix.pyi +82 -0
- simlar/indexes/_relevance.pyi +46 -0
- simlar/indexes/_simlar.pyi +54 -0
- simlar/indexes/_streaming.pyi +59 -0
- simlar/indexes/helix_index.py +126 -0
- simlar/indexes/lookup_index.py +78 -0
- simlar/indexes/registry.py +55 -0
- simlar/indexes/relevance_index.py +88 -0
- simlar/indexes/simlar_engine.py +113 -0
- simlar/indexes/streaming_index.py +91 -0
- simlar/integrations/__init__.py +0 -0
- simlar/integrations/haystack/simlar_document_store.py +401 -0
- simlar/integrations/haystack/simlar_retriever.py +64 -0
- simlar/integrations/langchain/langchain_retriever.py +49 -0
- simlar/integrations/langchain/simlar_vector_store.py +268 -0
- simlar/integrations/llama_index/simlar_retriever.py +186 -0
- simlar/integrations/llama_index/simlar_vector_store.py +248 -0
- simlar/persistence.py +5 -0
- simlar/py.typed +0 -0
- simlar-1.0.0.dist-info/METADATA +241 -0
- simlar-1.0.0.dist-info/RECORD +31 -0
- simlar-1.0.0.dist-info/WHEEL +5 -0
- simlar-1.0.0.dist-info/licenses/LICENSE +182 -0
- simlar-1.0.0.dist-info/licenses/NOTICE +21 -0
- simlar-1.0.0.dist-info/top_level.txt +1 -0
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]: ...
|
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,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
|