os-hybrid-kit 0.5.4__tar.gz
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.
- os_hybrid_kit-0.5.4/LICENSE +21 -0
- os_hybrid_kit-0.5.4/PKG-INFO +276 -0
- os_hybrid_kit-0.5.4/README.md +235 -0
- os_hybrid_kit-0.5.4/pyproject.toml +75 -0
- os_hybrid_kit-0.5.4/setup.cfg +4 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/__init__.py +42 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/adapters/__init__.py +5 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/adapters/dify/__init__.py +26 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/adapters/dify/retrieval.py +105 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/adapters/dify/vdb.py +91 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/client.py +313 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/config.py +148 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/integrations/__init__.py +16 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/integrations/_search.py +65 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/integrations/langchain.py +69 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/integrations/llama_index.py +59 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/mapping.py +65 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/pipeline.py +91 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/py.typed +0 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/query.py +124 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/results.py +70 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit/types.py +17 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit.egg-info/PKG-INFO +276 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit.egg-info/SOURCES.txt +36 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit.egg-info/dependency_links.txt +1 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit.egg-info/requires.txt +15 -0
- os_hybrid_kit-0.5.4/src/os_hybrid_kit.egg-info/top_level.txt +1 -0
- os_hybrid_kit-0.5.4/tests/test_client.py +483 -0
- os_hybrid_kit-0.5.4/tests/test_config.py +117 -0
- os_hybrid_kit-0.5.4/tests/test_dify_adapter.py +156 -0
- os_hybrid_kit-0.5.4/tests/test_integrations_install_error.py +70 -0
- os_hybrid_kit-0.5.4/tests/test_integrations_search.py +129 -0
- os_hybrid_kit-0.5.4/tests/test_langchain_retriever.py +82 -0
- os_hybrid_kit-0.5.4/tests/test_llama_index_retriever.py +77 -0
- os_hybrid_kit-0.5.4/tests/test_mapping.py +49 -0
- os_hybrid_kit-0.5.4/tests/test_pipeline.py +71 -0
- os_hybrid_kit-0.5.4/tests/test_query.py +155 -0
- os_hybrid_kit-0.5.4/tests/test_types.py +12 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zhu Lei
|
|
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.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: os-hybrid-kit
|
|
3
|
+
Version: 0.5.4
|
|
4
|
+
Summary: Portable OpenSearch hybrid search (BM25 + kNN + RRF / weighted normalization) with a thin Dify adapter and optional LangChain / LlamaIndex retrievers.
|
|
5
|
+
Author-email: Zhu Lei <woicyou@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kabishou11/os-hybrid-kit
|
|
8
|
+
Project-URL: Issues, https://github.com/kabishou11/os-hybrid-kit/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/kabishou11/os-hybrid-kit/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Documentation, https://github.com/kabishou11/os-hybrid-kit#readme
|
|
11
|
+
Project-URL: Source, https://github.com/kabishou11/os-hybrid-kit
|
|
12
|
+
Keywords: opensearch,hybrid-search,bm25,knn,rrf,dify,langchain,llama-index
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: opensearch-py<4,>=2.6
|
|
30
|
+
Requires-Dist: pydantic>=2.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
35
|
+
Provides-Extra: dify
|
|
36
|
+
Provides-Extra: langchain
|
|
37
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
38
|
+
Provides-Extra: llama-index
|
|
39
|
+
Requires-Dist: llama-index-core>=0.10; extra == "llama-index"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# os-hybrid-kit
|
|
43
|
+
|
|
44
|
+
Portable Python library for **OpenSearch hybrid search**: BM25 lexical match plus kNN vectors, fused with **RRF** or **weighted score normalization**.
|
|
45
|
+
|
|
46
|
+
[](https://github.com/kabishou11/os-hybrid-kit/actions/workflows/ci.yml)
|
|
47
|
+
[](https://pypi.org/project/os-hybrid-kit/)
|
|
48
|
+
[](https://pypi.org/project/os-hybrid-kit/)
|
|
49
|
+
[](https://github.com/kabishou11/os-hybrid-kit/blob/main/LICENSE)
|
|
50
|
+
|
|
51
|
+
**v0.5.4** — facade frozen; 0.5.x is polish and hardening. You bring the embedding model; the kit builds the mapping, upserts the search pipeline, and runs hybrid search.
|
|
52
|
+
|
|
53
|
+
[CHANGELOG](https://github.com/kabishou11/os-hybrid-kit/blob/main/CHANGELOG.md) · [Production checklist](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/PRODUCTION.md) · [Relevance and latency notes](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/NOTES.md) · [Integrations](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/INTEGRATIONS.md) · [Roadmap](https://github.com/kabishou11/os-hybrid-kit/blob/main/ROADMAP.md)
|
|
54
|
+
|
|
55
|
+
## Why
|
|
56
|
+
|
|
57
|
+
Keyword scores (BM25) and vector similarity live on different scales. Adding them naively ranks the wrong documents. OpenSearch 2.x solves this with a `hybrid` query and a **search pipeline** that fuses the two lists after the query phase.
|
|
58
|
+
|
|
59
|
+
## Install and 5-minute quickstart
|
|
60
|
+
|
|
61
|
+
Python 3.10+. Docker for the demo cluster.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install os-hybrid-kit
|
|
65
|
+
# from source:
|
|
66
|
+
python -m pip install git+https://github.com/kabishou11/os-hybrid-kit.git
|
|
67
|
+
# or from a clone:
|
|
68
|
+
python -m pip install -e .
|
|
69
|
+
docker compose up -d
|
|
70
|
+
python examples/seed_and_search.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The demo uses OpenSearch **2.19.6** (single node, security plugin off, `http://localhost:9200`) so both RRF and weighted fusion work. It hashes tokens into a tiny vector so you do not need an embedding model.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python examples/seed_and_search.py --fusion weighted
|
|
77
|
+
python examples/seed_and_search.py --query "waterproof boots for muddy paths"
|
|
78
|
+
python examples/compare_retrievers.py # BM25 vs kNN vs hybrid ranking
|
|
79
|
+
python examples/benchmark_retrievers.py # same three modes, wall time (ms)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If OpenSearch fails to boot on Linux: `sudo sysctl -w vm.max_map_count=262144`. Wait until `curl http://localhost:9200` returns cluster info. Tear down with `docker compose down` (or `make demo-down`). Example scripts exit with a `docker compose up -d` hint if the cluster is down.
|
|
83
|
+
|
|
84
|
+
## Library usage
|
|
85
|
+
|
|
86
|
+
**Primary facade:** `HybridKit`, `HybridConfig`, `FusionMethod`, `Hit`, `SearchResult`.
|
|
87
|
+
|
|
88
|
+
**Methods:** `ensure_index`, `exists_index`, `delete_index`, `upsert_pipeline`, `index_documents`, `lexical_search`, `knn_search`, `hybrid_search`. Alias helpers: `put_alias`, `delete_alias`, `get_alias`, `swap_alias`, `with_index`.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from os_hybrid_kit import FusionMethod, HybridConfig, HybridKit
|
|
92
|
+
|
|
93
|
+
config = HybridConfig(
|
|
94
|
+
hosts=["http://localhost:9200"],
|
|
95
|
+
index="docs",
|
|
96
|
+
dimension=384, # must match your embedding model
|
|
97
|
+
text_field="content",
|
|
98
|
+
vector_field="embedding",
|
|
99
|
+
fusion=FusionMethod.RRF, # or FusionMethod.WEIGHTED
|
|
100
|
+
pipeline_name="hybrid-search-pipeline",
|
|
101
|
+
lexical_weight=0.3, # weighted fusion; clause order is lexical then kNN
|
|
102
|
+
vector_weight=0.7,
|
|
103
|
+
exclude_vector=True, # drop the vector field from _source on search
|
|
104
|
+
)
|
|
105
|
+
# Or: HybridConfig.from_env() # OPENSEARCH_URL / OPENSEARCH_HOSTS,
|
|
106
|
+
# # OPENSEARCH_INDEX, OPENSEARCH_DIM
|
|
107
|
+
|
|
108
|
+
kit = HybridKit(config)
|
|
109
|
+
kit.delete_index() # no-op if the index is missing
|
|
110
|
+
kit.ensure_index(extra_properties={"title": {"type": "text"}})
|
|
111
|
+
kit.upsert_pipeline()
|
|
112
|
+
|
|
113
|
+
kit.index_documents(
|
|
114
|
+
[
|
|
115
|
+
{
|
|
116
|
+
"_id": "1",
|
|
117
|
+
"title": "Trail shoes",
|
|
118
|
+
"content": "Lightweight trail running shoes.",
|
|
119
|
+
"embedding": query_vector, # list[float], length == dimension
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = kit.hybrid_search("running shoes", query_vector, size=10)
|
|
125
|
+
for hit in result.hits: # Hit: id, score, source, index, raw
|
|
126
|
+
print(hit.score, hit.source["content"])
|
|
127
|
+
# result.texts("content") -> list[str]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`hybrid_search` sends a `hybrid` query with two clauses — `match` on the text field, then `knn` on the vector field — and sets `search_pipeline` so OpenSearch fuses the lists. That is the main path. `lexical_search(query)` and `knn_search(embedding)` hit the same index without a pipeline (pure BM25 `match`, raw `knn`). All three accept `filter_query` and `_source` includes/excludes.
|
|
131
|
+
|
|
132
|
+
Embeddings are **yours**; the kit does not call a model or the Neural Search `neural` query. Pass an `EmbedFn` (`(text: str) -> Sequence[float]`) wherever you need to embed a query (the Dify adapter does). An empty embedding, or a vector whose length does not match `config.dimension`, raises `ValueError`. Empty or whitespace-only `query`, alias, and index names on `lexical_search` / `hybrid_search` / alias helpers / `with_index` also raise `ValueError` before OpenSearch is called.
|
|
133
|
+
|
|
134
|
+
`exists_index` / `ensure_index` / `delete_index` operate on `config.index`. `from_env()` reads `OPENSEARCH_HOSTS` (comma-separated, takes precedence) or `OPENSEARCH_URL`, plus `OPENSEARCH_INDEX` and `OPENSEARCH_DIM`. Keyword arguments override the environment.
|
|
135
|
+
|
|
136
|
+
**Builders** (advanced — use these if you already have an OpenSearch client):
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from os_hybrid_kit import (
|
|
140
|
+
EmbedFn,
|
|
141
|
+
build_hybrid_query,
|
|
142
|
+
build_index_body,
|
|
143
|
+
build_knn_query,
|
|
144
|
+
build_knn_vector_property,
|
|
145
|
+
build_lexical_query,
|
|
146
|
+
build_opensearch_client,
|
|
147
|
+
build_pipeline_body,
|
|
148
|
+
build_rrf_pipeline_body,
|
|
149
|
+
build_weighted_pipeline_body,
|
|
150
|
+
parse_search_response,
|
|
151
|
+
upsert_search_pipeline,
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Aliases and cutover
|
|
156
|
+
|
|
157
|
+
`config.index` is the OpenSearch target for mapping, bulk, and search. It may be a **concrete index** or an **alias**. Pointing reads at an alias lets you rebuild a new index and cut over without changing query code.
|
|
158
|
+
|
|
159
|
+
Create the concrete index first (`ensure_index`). Attach an alias with `put_alias` / `swap_alias`, then read through it with `with_index`. Do not call `ensure_index` on an alias name: if the alias is missing, OpenSearch may create a concrete index of that name.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
kit = HybridKit(config) # config.index == "docs-v1"
|
|
163
|
+
kit.ensure_index()
|
|
164
|
+
kit.put_alias("docs-read") # alias -> docs-v1
|
|
165
|
+
kit.with_index("docs-read").hybrid_search(query, embedding)
|
|
166
|
+
|
|
167
|
+
blue = kit.with_index("docs-v2")
|
|
168
|
+
blue.ensure_index()
|
|
169
|
+
blue.index_documents([...])
|
|
170
|
+
kit.swap_alias("docs-read", "docs-v2", old_index="docs-v1")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`swap_alias` issues one `_aliases` request (remove old + add new) when `old_index` is passed. If `old_index` is omitted, the kit looks up current targets first, then updates. A missing alias raises `AliasNotFoundError` (`ValueError`) from `get_alias` / `delete_alias`; `swap_alias` still adds when the alias is missing. This is not a full index-management product: no `is_write_index` policy, no ILM.
|
|
174
|
+
|
|
175
|
+
## Benchmark
|
|
176
|
+
|
|
177
|
+
`examples/benchmark_retrievers.py` seeds a fixed demo corpus and prints BM25 vs kNN vs hybrid hit ids, scores, and wall time in milliseconds. It is a stopwatch on your machine, not an eval harness and not a quality claim. `examples/compare_retrievers.py` is the ranking side-by-side (no timings).
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
docker compose up -d
|
|
181
|
+
python examples/benchmark_retrievers.py
|
|
182
|
+
python examples/benchmark_retrievers.py --url http://localhost:9200 --n-queries 10 --fusion weighted
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Stable API (frozen since 0.3 / current 0.5.x)
|
|
186
|
+
|
|
187
|
+
These names have been frozen since 0.3. Additive extras (0.4 LangChain / LlamaIndex retrievers, 0.5 alias helpers) do not rename this facade. Breaking renames will not.
|
|
188
|
+
|
|
189
|
+
| Kind | Names |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| Types | `HybridKit`, `HybridConfig`, `FusionMethod`, `Hit`, `SearchResult` |
|
|
192
|
+
| Methods | `ensure_index`, `exists_index`, `delete_index`, `upsert_pipeline`, `index_documents`, `lexical_search`, `knn_search`, `hybrid_search` |
|
|
193
|
+
| Additive (0.5) | `put_alias`, `delete_alias`, `get_alias`, `swap_alias`, `with_index` |
|
|
194
|
+
| Fusion values | `FusionMethod.RRF` (`"rrf"`), `FusionMethod.WEIGHTED` (`"weighted"`) |
|
|
195
|
+
| Typing | `EmbedFn` (optional protocol for query embedders) |
|
|
196
|
+
| Advanced | `build_*` helpers, `parse_search_response`, `upsert_search_pipeline`, `build_opensearch_client` |
|
|
197
|
+
|
|
198
|
+
`Hit` fields: `id`, `score`, `source`, `index`, and `raw` (the original OpenSearch hit dict; optional if you construct a `Hit` yourself — `parse_search_response` always fills it). `SearchResult` fields: `hits`, `total`, `max_score`, `raw`, plus `texts(field)` which returns `str(hit.source.get(field, ""))` for each hit.
|
|
199
|
+
|
|
200
|
+
## Fusion
|
|
201
|
+
|
|
202
|
+
| Fusion | Processor | Combines | OpenSearch |
|
|
203
|
+
| --- | --- | --- | --- |
|
|
204
|
+
| Reciprocal rank fusion (`FusionMethod.RRF`) | `score-ranker-processor` | ranks, not raw scores | 2.19+ |
|
|
205
|
+
| Weighted normalization (`FusionMethod.WEIGHTED`) | `normalization-processor` | min-max / L2 / z-score, then a weighted mean | 2.11+ |
|
|
206
|
+
|
|
207
|
+
Default weighted weights are `lexical_weight=0.3`, `vector_weight=0.7` and must sum to 1.0. Clause order is lexical then kNN, so those weights line up with the hybrid query.
|
|
208
|
+
|
|
209
|
+
## Integrations (optional)
|
|
210
|
+
|
|
211
|
+
Thin retrievers around the frozen `HybridKit` facade. Core install does not pull these in.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install "os-hybrid-kit[langchain]"
|
|
215
|
+
pip install "os-hybrid-kit[llama-index]"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from os_hybrid_kit import HybridConfig, HybridKit
|
|
220
|
+
from os_hybrid_kit.integrations.langchain import HybridKitRetriever
|
|
221
|
+
|
|
222
|
+
kit = HybridKit(HybridConfig(dimension=384, index="docs"))
|
|
223
|
+
retriever = HybridKitRetriever(
|
|
224
|
+
kit,
|
|
225
|
+
embed_fn, # (text: str) -> Sequence[float]
|
|
226
|
+
search_kwargs={"size": 10, "mode": "hybrid"}, # hybrid | lexical | knn
|
|
227
|
+
)
|
|
228
|
+
docs = retriever.invoke("waterproof trail shoes")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from os_hybrid_kit.integrations.llama_index import HybridKitRetriever
|
|
233
|
+
|
|
234
|
+
retriever = HybridKitRetriever(kit, embed_fn, search_kwargs={"size": 10})
|
|
235
|
+
nodes = retriever.retrieve("waterproof trail shoes")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Importing a submodule without its extra raises `os_hybrid_kit.integrations.InstallError`. Details: [docs/INTEGRATIONS.md](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/INTEGRATIONS.md).
|
|
239
|
+
|
|
240
|
+
## Dify adapter
|
|
241
|
+
|
|
242
|
+
`os_hybrid_kit.adapters.dify` (`src/os_hybrid_kit/adapters/dify/`) is a stub, not a marketplace plugin. Source checkouts can still import `adapters.dify`; that path warns and is not included in the wheel.
|
|
243
|
+
|
|
244
|
+
- **External knowledge:** `retrieve(kit, request, embed_fn)` accepts Dify's `POST /retrieval` JSON and returns `{ "records": [ { content, score, title, metadata } ] }`. Dify only sends text, so you pass an `EmbedFn`.
|
|
245
|
+
- **VDB stub:** `HybridVectorStore` exposes `add_texts`, `hybrid_search`, `search_by_vector`, and `search_by_full_text` for a plugin you maintain.
|
|
246
|
+
|
|
247
|
+
RRF scores are small (they are sums of `1 / (rank_constant + rank)`). If Dify's score threshold is `0.5`, RRF hits will be filtered out. Use threshold `0` with RRF, or switch to weighted fusion (scores are typically in `[0, 1]`). Details: [docs/PRODUCTION.md](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/PRODUCTION.md).
|
|
248
|
+
|
|
249
|
+
## Tests
|
|
250
|
+
|
|
251
|
+
Unit tests run on every push. An `integration` job starts OpenSearch 2.19.6 with `docker compose` and runs `pytest -m integration`. You do not need Docker for unit tests. Retriever tests skip unless the matching extra is installed.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
python -m pip install -e ".[dev]"
|
|
255
|
+
pytest -m "not integration" # unit tests; no Docker / live cluster
|
|
256
|
+
# or: make test && make lint
|
|
257
|
+
pytest -m integration # live cluster; CI sets RUN_INTEGRATION=1
|
|
258
|
+
# retriever tests skip unless extras are installed:
|
|
259
|
+
make test-extras # pip install -e ".[dev,langchain,llama-index]" then pytest
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Live tests skip unless `RUN_INTEGRATION=1`. Default URL is `OPENSEARCH_URL` or `http://localhost:9200`.
|
|
263
|
+
|
|
264
|
+
## What this is not
|
|
265
|
+
|
|
266
|
+
- A RAG UI, chat app, or document-ingestion product
|
|
267
|
+
- An MCP skills bridge
|
|
268
|
+
- An evaluation harness or RAGFlow eval kit
|
|
269
|
+
- A LiteLLM / model gateway
|
|
270
|
+
- ColQwen, late-interaction, or multimodal retrieval
|
|
271
|
+
- Guardrails or policy enforcement
|
|
272
|
+
- A packaged Dify marketplace plugin (the adapter is the retrieval contract plus a VDB stub)
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT. See [LICENSE](https://github.com/kabishou11/os-hybrid-kit/blob/main/LICENSE).
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# os-hybrid-kit
|
|
2
|
+
|
|
3
|
+
Portable Python library for **OpenSearch hybrid search**: BM25 lexical match plus kNN vectors, fused with **RRF** or **weighted score normalization**.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/kabishou11/os-hybrid-kit/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/os-hybrid-kit/)
|
|
7
|
+
[](https://pypi.org/project/os-hybrid-kit/)
|
|
8
|
+
[](https://github.com/kabishou11/os-hybrid-kit/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
**v0.5.4** — facade frozen; 0.5.x is polish and hardening. You bring the embedding model; the kit builds the mapping, upserts the search pipeline, and runs hybrid search.
|
|
11
|
+
|
|
12
|
+
[CHANGELOG](https://github.com/kabishou11/os-hybrid-kit/blob/main/CHANGELOG.md) · [Production checklist](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/PRODUCTION.md) · [Relevance and latency notes](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/NOTES.md) · [Integrations](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/INTEGRATIONS.md) · [Roadmap](https://github.com/kabishou11/os-hybrid-kit/blob/main/ROADMAP.md)
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Keyword scores (BM25) and vector similarity live on different scales. Adding them naively ranks the wrong documents. OpenSearch 2.x solves this with a `hybrid` query and a **search pipeline** that fuses the two lists after the query phase.
|
|
17
|
+
|
|
18
|
+
## Install and 5-minute quickstart
|
|
19
|
+
|
|
20
|
+
Python 3.10+. Docker for the demo cluster.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m pip install os-hybrid-kit
|
|
24
|
+
# from source:
|
|
25
|
+
python -m pip install git+https://github.com/kabishou11/os-hybrid-kit.git
|
|
26
|
+
# or from a clone:
|
|
27
|
+
python -m pip install -e .
|
|
28
|
+
docker compose up -d
|
|
29
|
+
python examples/seed_and_search.py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The demo uses OpenSearch **2.19.6** (single node, security plugin off, `http://localhost:9200`) so both RRF and weighted fusion work. It hashes tokens into a tiny vector so you do not need an embedding model.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python examples/seed_and_search.py --fusion weighted
|
|
36
|
+
python examples/seed_and_search.py --query "waterproof boots for muddy paths"
|
|
37
|
+
python examples/compare_retrievers.py # BM25 vs kNN vs hybrid ranking
|
|
38
|
+
python examples/benchmark_retrievers.py # same three modes, wall time (ms)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If OpenSearch fails to boot on Linux: `sudo sysctl -w vm.max_map_count=262144`. Wait until `curl http://localhost:9200` returns cluster info. Tear down with `docker compose down` (or `make demo-down`). Example scripts exit with a `docker compose up -d` hint if the cluster is down.
|
|
42
|
+
|
|
43
|
+
## Library usage
|
|
44
|
+
|
|
45
|
+
**Primary facade:** `HybridKit`, `HybridConfig`, `FusionMethod`, `Hit`, `SearchResult`.
|
|
46
|
+
|
|
47
|
+
**Methods:** `ensure_index`, `exists_index`, `delete_index`, `upsert_pipeline`, `index_documents`, `lexical_search`, `knn_search`, `hybrid_search`. Alias helpers: `put_alias`, `delete_alias`, `get_alias`, `swap_alias`, `with_index`.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from os_hybrid_kit import FusionMethod, HybridConfig, HybridKit
|
|
51
|
+
|
|
52
|
+
config = HybridConfig(
|
|
53
|
+
hosts=["http://localhost:9200"],
|
|
54
|
+
index="docs",
|
|
55
|
+
dimension=384, # must match your embedding model
|
|
56
|
+
text_field="content",
|
|
57
|
+
vector_field="embedding",
|
|
58
|
+
fusion=FusionMethod.RRF, # or FusionMethod.WEIGHTED
|
|
59
|
+
pipeline_name="hybrid-search-pipeline",
|
|
60
|
+
lexical_weight=0.3, # weighted fusion; clause order is lexical then kNN
|
|
61
|
+
vector_weight=0.7,
|
|
62
|
+
exclude_vector=True, # drop the vector field from _source on search
|
|
63
|
+
)
|
|
64
|
+
# Or: HybridConfig.from_env() # OPENSEARCH_URL / OPENSEARCH_HOSTS,
|
|
65
|
+
# # OPENSEARCH_INDEX, OPENSEARCH_DIM
|
|
66
|
+
|
|
67
|
+
kit = HybridKit(config)
|
|
68
|
+
kit.delete_index() # no-op if the index is missing
|
|
69
|
+
kit.ensure_index(extra_properties={"title": {"type": "text"}})
|
|
70
|
+
kit.upsert_pipeline()
|
|
71
|
+
|
|
72
|
+
kit.index_documents(
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
"_id": "1",
|
|
76
|
+
"title": "Trail shoes",
|
|
77
|
+
"content": "Lightweight trail running shoes.",
|
|
78
|
+
"embedding": query_vector, # list[float], length == dimension
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = kit.hybrid_search("running shoes", query_vector, size=10)
|
|
84
|
+
for hit in result.hits: # Hit: id, score, source, index, raw
|
|
85
|
+
print(hit.score, hit.source["content"])
|
|
86
|
+
# result.texts("content") -> list[str]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`hybrid_search` sends a `hybrid` query with two clauses — `match` on the text field, then `knn` on the vector field — and sets `search_pipeline` so OpenSearch fuses the lists. That is the main path. `lexical_search(query)` and `knn_search(embedding)` hit the same index without a pipeline (pure BM25 `match`, raw `knn`). All three accept `filter_query` and `_source` includes/excludes.
|
|
90
|
+
|
|
91
|
+
Embeddings are **yours**; the kit does not call a model or the Neural Search `neural` query. Pass an `EmbedFn` (`(text: str) -> Sequence[float]`) wherever you need to embed a query (the Dify adapter does). An empty embedding, or a vector whose length does not match `config.dimension`, raises `ValueError`. Empty or whitespace-only `query`, alias, and index names on `lexical_search` / `hybrid_search` / alias helpers / `with_index` also raise `ValueError` before OpenSearch is called.
|
|
92
|
+
|
|
93
|
+
`exists_index` / `ensure_index` / `delete_index` operate on `config.index`. `from_env()` reads `OPENSEARCH_HOSTS` (comma-separated, takes precedence) or `OPENSEARCH_URL`, plus `OPENSEARCH_INDEX` and `OPENSEARCH_DIM`. Keyword arguments override the environment.
|
|
94
|
+
|
|
95
|
+
**Builders** (advanced — use these if you already have an OpenSearch client):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from os_hybrid_kit import (
|
|
99
|
+
EmbedFn,
|
|
100
|
+
build_hybrid_query,
|
|
101
|
+
build_index_body,
|
|
102
|
+
build_knn_query,
|
|
103
|
+
build_knn_vector_property,
|
|
104
|
+
build_lexical_query,
|
|
105
|
+
build_opensearch_client,
|
|
106
|
+
build_pipeline_body,
|
|
107
|
+
build_rrf_pipeline_body,
|
|
108
|
+
build_weighted_pipeline_body,
|
|
109
|
+
parse_search_response,
|
|
110
|
+
upsert_search_pipeline,
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Aliases and cutover
|
|
115
|
+
|
|
116
|
+
`config.index` is the OpenSearch target for mapping, bulk, and search. It may be a **concrete index** or an **alias**. Pointing reads at an alias lets you rebuild a new index and cut over without changing query code.
|
|
117
|
+
|
|
118
|
+
Create the concrete index first (`ensure_index`). Attach an alias with `put_alias` / `swap_alias`, then read through it with `with_index`. Do not call `ensure_index` on an alias name: if the alias is missing, OpenSearch may create a concrete index of that name.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
kit = HybridKit(config) # config.index == "docs-v1"
|
|
122
|
+
kit.ensure_index()
|
|
123
|
+
kit.put_alias("docs-read") # alias -> docs-v1
|
|
124
|
+
kit.with_index("docs-read").hybrid_search(query, embedding)
|
|
125
|
+
|
|
126
|
+
blue = kit.with_index("docs-v2")
|
|
127
|
+
blue.ensure_index()
|
|
128
|
+
blue.index_documents([...])
|
|
129
|
+
kit.swap_alias("docs-read", "docs-v2", old_index="docs-v1")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`swap_alias` issues one `_aliases` request (remove old + add new) when `old_index` is passed. If `old_index` is omitted, the kit looks up current targets first, then updates. A missing alias raises `AliasNotFoundError` (`ValueError`) from `get_alias` / `delete_alias`; `swap_alias` still adds when the alias is missing. This is not a full index-management product: no `is_write_index` policy, no ILM.
|
|
133
|
+
|
|
134
|
+
## Benchmark
|
|
135
|
+
|
|
136
|
+
`examples/benchmark_retrievers.py` seeds a fixed demo corpus and prints BM25 vs kNN vs hybrid hit ids, scores, and wall time in milliseconds. It is a stopwatch on your machine, not an eval harness and not a quality claim. `examples/compare_retrievers.py` is the ranking side-by-side (no timings).
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
docker compose up -d
|
|
140
|
+
python examples/benchmark_retrievers.py
|
|
141
|
+
python examples/benchmark_retrievers.py --url http://localhost:9200 --n-queries 10 --fusion weighted
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Stable API (frozen since 0.3 / current 0.5.x)
|
|
145
|
+
|
|
146
|
+
These names have been frozen since 0.3. Additive extras (0.4 LangChain / LlamaIndex retrievers, 0.5 alias helpers) do not rename this facade. Breaking renames will not.
|
|
147
|
+
|
|
148
|
+
| Kind | Names |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| Types | `HybridKit`, `HybridConfig`, `FusionMethod`, `Hit`, `SearchResult` |
|
|
151
|
+
| Methods | `ensure_index`, `exists_index`, `delete_index`, `upsert_pipeline`, `index_documents`, `lexical_search`, `knn_search`, `hybrid_search` |
|
|
152
|
+
| Additive (0.5) | `put_alias`, `delete_alias`, `get_alias`, `swap_alias`, `with_index` |
|
|
153
|
+
| Fusion values | `FusionMethod.RRF` (`"rrf"`), `FusionMethod.WEIGHTED` (`"weighted"`) |
|
|
154
|
+
| Typing | `EmbedFn` (optional protocol for query embedders) |
|
|
155
|
+
| Advanced | `build_*` helpers, `parse_search_response`, `upsert_search_pipeline`, `build_opensearch_client` |
|
|
156
|
+
|
|
157
|
+
`Hit` fields: `id`, `score`, `source`, `index`, and `raw` (the original OpenSearch hit dict; optional if you construct a `Hit` yourself — `parse_search_response` always fills it). `SearchResult` fields: `hits`, `total`, `max_score`, `raw`, plus `texts(field)` which returns `str(hit.source.get(field, ""))` for each hit.
|
|
158
|
+
|
|
159
|
+
## Fusion
|
|
160
|
+
|
|
161
|
+
| Fusion | Processor | Combines | OpenSearch |
|
|
162
|
+
| --- | --- | --- | --- |
|
|
163
|
+
| Reciprocal rank fusion (`FusionMethod.RRF`) | `score-ranker-processor` | ranks, not raw scores | 2.19+ |
|
|
164
|
+
| Weighted normalization (`FusionMethod.WEIGHTED`) | `normalization-processor` | min-max / L2 / z-score, then a weighted mean | 2.11+ |
|
|
165
|
+
|
|
166
|
+
Default weighted weights are `lexical_weight=0.3`, `vector_weight=0.7` and must sum to 1.0. Clause order is lexical then kNN, so those weights line up with the hybrid query.
|
|
167
|
+
|
|
168
|
+
## Integrations (optional)
|
|
169
|
+
|
|
170
|
+
Thin retrievers around the frozen `HybridKit` facade. Core install does not pull these in.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pip install "os-hybrid-kit[langchain]"
|
|
174
|
+
pip install "os-hybrid-kit[llama-index]"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from os_hybrid_kit import HybridConfig, HybridKit
|
|
179
|
+
from os_hybrid_kit.integrations.langchain import HybridKitRetriever
|
|
180
|
+
|
|
181
|
+
kit = HybridKit(HybridConfig(dimension=384, index="docs"))
|
|
182
|
+
retriever = HybridKitRetriever(
|
|
183
|
+
kit,
|
|
184
|
+
embed_fn, # (text: str) -> Sequence[float]
|
|
185
|
+
search_kwargs={"size": 10, "mode": "hybrid"}, # hybrid | lexical | knn
|
|
186
|
+
)
|
|
187
|
+
docs = retriever.invoke("waterproof trail shoes")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from os_hybrid_kit.integrations.llama_index import HybridKitRetriever
|
|
192
|
+
|
|
193
|
+
retriever = HybridKitRetriever(kit, embed_fn, search_kwargs={"size": 10})
|
|
194
|
+
nodes = retriever.retrieve("waterproof trail shoes")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Importing a submodule without its extra raises `os_hybrid_kit.integrations.InstallError`. Details: [docs/INTEGRATIONS.md](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/INTEGRATIONS.md).
|
|
198
|
+
|
|
199
|
+
## Dify adapter
|
|
200
|
+
|
|
201
|
+
`os_hybrid_kit.adapters.dify` (`src/os_hybrid_kit/adapters/dify/`) is a stub, not a marketplace plugin. Source checkouts can still import `adapters.dify`; that path warns and is not included in the wheel.
|
|
202
|
+
|
|
203
|
+
- **External knowledge:** `retrieve(kit, request, embed_fn)` accepts Dify's `POST /retrieval` JSON and returns `{ "records": [ { content, score, title, metadata } ] }`. Dify only sends text, so you pass an `EmbedFn`.
|
|
204
|
+
- **VDB stub:** `HybridVectorStore` exposes `add_texts`, `hybrid_search`, `search_by_vector`, and `search_by_full_text` for a plugin you maintain.
|
|
205
|
+
|
|
206
|
+
RRF scores are small (they are sums of `1 / (rank_constant + rank)`). If Dify's score threshold is `0.5`, RRF hits will be filtered out. Use threshold `0` with RRF, or switch to weighted fusion (scores are typically in `[0, 1]`). Details: [docs/PRODUCTION.md](https://github.com/kabishou11/os-hybrid-kit/blob/main/docs/PRODUCTION.md).
|
|
207
|
+
|
|
208
|
+
## Tests
|
|
209
|
+
|
|
210
|
+
Unit tests run on every push. An `integration` job starts OpenSearch 2.19.6 with `docker compose` and runs `pytest -m integration`. You do not need Docker for unit tests. Retriever tests skip unless the matching extra is installed.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
python -m pip install -e ".[dev]"
|
|
214
|
+
pytest -m "not integration" # unit tests; no Docker / live cluster
|
|
215
|
+
# or: make test && make lint
|
|
216
|
+
pytest -m integration # live cluster; CI sets RUN_INTEGRATION=1
|
|
217
|
+
# retriever tests skip unless extras are installed:
|
|
218
|
+
make test-extras # pip install -e ".[dev,langchain,llama-index]" then pytest
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Live tests skip unless `RUN_INTEGRATION=1`. Default URL is `OPENSEARCH_URL` or `http://localhost:9200`.
|
|
222
|
+
|
|
223
|
+
## What this is not
|
|
224
|
+
|
|
225
|
+
- A RAG UI, chat app, or document-ingestion product
|
|
226
|
+
- An MCP skills bridge
|
|
227
|
+
- An evaluation harness or RAGFlow eval kit
|
|
228
|
+
- A LiteLLM / model gateway
|
|
229
|
+
- ColQwen, late-interaction, or multimodal retrieval
|
|
230
|
+
- Guardrails or policy enforcement
|
|
231
|
+
- A packaged Dify marketplace plugin (the adapter is the retrieval contract plus a VDB stub)
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT. See [LICENSE](https://github.com/kabishou11/os-hybrid-kit/blob/main/LICENSE).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "os-hybrid-kit"
|
|
7
|
+
version = "0.5.4"
|
|
8
|
+
description = "Portable OpenSearch hybrid search (BM25 + kNN + RRF / weighted normalization) with a thin Dify adapter and optional LangChain / LlamaIndex retrievers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Zhu Lei", email = "woicyou@gmail.com" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["opensearch", "hybrid-search", "bm25", "knn", "rrf", "dify", "langchain", "llama-index"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Topic :: Text Processing :: Indexing",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"opensearch-py>=2.6,<4",
|
|
32
|
+
"pydantic>=2.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-cov>=5.0",
|
|
39
|
+
"ruff>=0.6",
|
|
40
|
+
]
|
|
41
|
+
dify = []
|
|
42
|
+
langchain = ["langchain-core>=0.2"]
|
|
43
|
+
llama-index = ["llama-index-core>=0.10"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/kabishou11/os-hybrid-kit"
|
|
47
|
+
Issues = "https://github.com/kabishou11/os-hybrid-kit/issues"
|
|
48
|
+
Changelog = "https://github.com/kabishou11/os-hybrid-kit/blob/main/CHANGELOG.md"
|
|
49
|
+
Documentation = "https://github.com/kabishou11/os-hybrid-kit#readme"
|
|
50
|
+
Source = "https://github.com/kabishou11/os-hybrid-kit"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.package-data]
|
|
56
|
+
os_hybrid_kit = ["py.typed"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
markers = [
|
|
61
|
+
"integration: tests that require a live OpenSearch cluster",
|
|
62
|
+
]
|
|
63
|
+
addopts = "-q"
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py310"
|
|
68
|
+
src = ["src", "adapters", "tests", "examples"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
select = ["E", "F", "I", "UP", "W"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint.per-file-ignores]
|
|
74
|
+
"tests/*" = ["E501"]
|
|
75
|
+
"examples/*" = ["E501"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Portable OpenSearch hybrid search kit (BM25 + kNN + RRF / weighted fusion).
|
|
2
|
+
|
|
3
|
+
Primary facade: ``HybridKit``, ``HybridConfig``, ``FusionMethod``, ``Hit``,
|
|
4
|
+
``SearchResult``. Mapping, pipeline, and query ``build_*`` helpers are advanced.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from os_hybrid_kit.client import AliasNotFoundError, HybridKit, build_opensearch_client
|
|
8
|
+
from os_hybrid_kit.config import FusionMethod, HybridConfig
|
|
9
|
+
from os_hybrid_kit.mapping import build_index_body, build_knn_vector_property
|
|
10
|
+
from os_hybrid_kit.pipeline import (
|
|
11
|
+
build_pipeline_body,
|
|
12
|
+
build_rrf_pipeline_body,
|
|
13
|
+
build_weighted_pipeline_body,
|
|
14
|
+
upsert_search_pipeline,
|
|
15
|
+
)
|
|
16
|
+
from os_hybrid_kit.query import build_hybrid_query, build_knn_query, build_lexical_query
|
|
17
|
+
from os_hybrid_kit.results import Hit, SearchResult, parse_search_response
|
|
18
|
+
from os_hybrid_kit.types import EmbedFn
|
|
19
|
+
|
|
20
|
+
__version__ = "0.5.4"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AliasNotFoundError",
|
|
24
|
+
"EmbedFn",
|
|
25
|
+
"FusionMethod",
|
|
26
|
+
"Hit",
|
|
27
|
+
"HybridConfig",
|
|
28
|
+
"HybridKit",
|
|
29
|
+
"SearchResult",
|
|
30
|
+
"__version__",
|
|
31
|
+
"build_hybrid_query",
|
|
32
|
+
"build_index_body",
|
|
33
|
+
"build_knn_query",
|
|
34
|
+
"build_knn_vector_property",
|
|
35
|
+
"build_lexical_query",
|
|
36
|
+
"build_opensearch_client",
|
|
37
|
+
"build_pipeline_body",
|
|
38
|
+
"build_rrf_pipeline_body",
|
|
39
|
+
"build_weighted_pipeline_body",
|
|
40
|
+
"parse_search_response",
|
|
41
|
+
"upsert_search_pipeline",
|
|
42
|
+
]
|