Metadata-Version: 2.2
Name: brinicle
Version: 0.0.6
Summary: brinicle is a C++ vector index engine (ANN library) optimized for disk-first, low-RAM similarity search.
Keywords: brinicle,vector,retrieval,RAG,bicardinal
Author-Email: bicardinal <support@bicardinal.com>
License: Apache-2.0
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Project-URL: Homepage, https://github.com/bicardinal/brinicle
Project-URL: Issues, https://github.com/bicardinal/brinicle/issues
Requires-Python: >=3.10
Requires-Dist: numpy
Requires-Dist: tokenizers
Requires-Dist: orjson
Requires-Dist: pybind11
Provides-Extra: server
Requires-Dist: fastapi; extra == "server"
Requires-Dist: uvicorn[standard]; extra == "server"
Requires-Dist: pydantic; extra == "server"
Requires-Dist: python-multipart; extra == "server"
Description-Content-Type: text/markdown

![Version 0.0.6](https://img.shields.io/badge/Version-0.0.6-green.svg)
![Python 3.12.x](https://img.shields.io/badge/Python-3.12.x-green.svg)
![Apache-2.0 License](https://img.shields.io/badge/License-Apache2.0-green.svg)

# brinicle

**brinicle** is a C++ retrieval engine built around **disk-first, low-RAM HNSW search**.

It supports:

- raw vector similarity search
- lexical, semantic, and hybrid search for structured items through one HNSW index
- autocomplete and query suggestion search

---

## Benchmark

brinicle is designed for constrained environments where loading the full index into RAM is not practical.

In a 256MB RAM / 1 CPU container using MNIST 60K vectors:

| System | Outcome |
|---|---|
| brinicle | PASS |
| chroma | PASS |
| qdrant | OOMKilled |
| weaviate | OOMKilled |
| milvus | OOMKilled |

On SIFT 1M vectors, using the same in-process deployment model as FAISS and hnswlib:

| System | Build (s) | Recall@10 | Avg latency (ms) | QPS |
|---|---:|---:|---:|---:|
| faiss | 237.282 | 0.96999 | 0.092 | 10857.43 |
| hnswlib | 241.301 | 0.96364 | 0.093 | 10711.86 |
| brinicle | 243.75 | 0.96989 | 0.103 | 9730.65 |

In this benchmark suite, brinicle stays close to FAISS and hnswlib latency while using a disk-backed index design.

![Memory usage comparison](https://brinicle.bicardinal.com/blow/memory_bars.png)

See the benchmark: [brinicle benchmark](https://brinicle.bicardinal.com/benchmark)

---

## Install

Install from PyPI:

```bash
pip install brinicle
```

Or build from source:

```bash
git clone https://github.com/bicardinal/brinicle.git
cd brinicle
pip install -e .
```

---

## Engines

brinicle exposes three engines with the same lifecycle:

```python
client.init(...)
client.ingest(...)
client.finalize()
client.search(...)
```

| Engine | Use case | Input |
|---|---|---|
| `VectorEngine` | Raw ANN vector search | `float32` vectors |
| `ItemSearchEngine` | Lexical / semantic / hybrid item search | title, category, subcategory, attributes, optional vectors |
| `AutocompleteEngine` | Query/title suggestions | suggestion text |

---

## Features

- Disk-first HNSW vector search
- Low-RAM indexing and querying
- Streaming-first ingest: one vector/item/suggestion at a time
- Insert, upsert, delete, and compact rebuild
- Raw vector search through `VectorEngine`
- Structured item search through `ItemSearchEngine`
- Lexical, semantic, and hybrid item search using one HNSW index
- Alpha-controlled item search: lexical-only, semantic-only, or hybrid
- Autocomplete/query suggestion search through `AutocompleteEngine`

---

## Vector search

Use `VectorEngine` when you already have embeddings or numeric vectors.

```python
import numpy as np
import brinicle

D = 2
n = 5

X = np.random.randn(n, D).astype(np.float32)
Q = np.random.randn(D).astype(np.float32)

engine = brinicle.VectorEngine(
    "vector_index",
    dim=D,
    delta_ratio=0.1,
)

engine.init(mode="build")

for eid in range(n):
    engine.ingest(str(eid), X[eid])

engine.finalize()

print(engine.search(Q, k=10))
```

`search(...)` returns a list of external ids:

```python
["3", "1", "0"]
```

To return distances too:

```python
print(engine.search_with_distance(Q, k=10))
```

To do batch search:
```python
engine.search_batch(Qs)
```

---

## Insert

```python
Y = np.random.randn(5, D).astype(np.float32)

engine.init(mode="insert")

for eid in range(5):
    engine.ingest(str(eid) + "x", Y[eid])

engine.finalize()

print(engine.search(Q, k=10))
```

---

## Upsert

```python
Y = np.random.randn(5, D).astype(np.float32)

engine.init(mode="upsert")

for eid in range(5):
    engine.ingest(str(eid), Y[eid])

engine.finalize()

print(engine.search(Q, k=10))
```

---

## Delete

```python
engine.delete_items(["1", "4"])

print(engine.search(Q, k=10))
```

---

## Rebuild / optimize

```python
engine.optimize_graph()

print(engine.search(Q, k=10))
```

---

## Item search

`ItemSearchEngine` searches catalog-like records with titles, metadata, and optional semantic vectors.

Each item can contain:

- `title`
- `category`
- `subcategory`
- `attributes`
- an optional semantic vector

Only `title` is required. The other fields are optional.

`ItemSearchEngine` can run in three practical modes:

| Mode | How to use it |
|---|---|
| Lexical-only item search | Use structured fields only and set `alpha=0.0` |
| Semantic-only item search | Provide vectors and set `alpha=1.0` |
| Hybrid item search | Provide structured fields and vectors, then use an `alpha` between `0.0` and `1.0` |

brinicle does not build separate lexical and vector indexes for item search. Structured lexical signals and optional semantic vectors are encoded into one numeric representation and searched through the same HNSW graph.

---

### Lexical item search

Use lexical item search when you want structured catalog search without external embeddings.

```python
import brinicle

engine = brinicle.ItemSearchEngine(
    "item_index",
    dim=96, # the larger, the more embedding space, the less truncation
    alpha=0.0,  # lexical-only
)

engine.init(mode="build")

engine.ingest(
    external_id="p1",
    title="Apple iPhone 15 Pro Max 256GB Natural Titanium",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Apple",
        "storage": "256GB",
        "color": "Natural Titanium",
    },
)

engine.ingest(
    external_id="p2",
    title="Samsung Galaxy S24 Ultra 512GB Black",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Samsung",
        "storage": "512GB",
        "color": "Black",
    },
)

engine.finalize()

print(engine.search("iphone 15 pro max", k=10))
```

---

### Hybrid item search

Use hybrid item search when you want exact structured signals and semantic similarity in the same retrieval path.

```python
import numpy as np
import brinicle

VECTOR_DIM = 384

engine = brinicle.ItemSearchEngine(
    "hybrid_item_index",
    dim=96,
    vector_dim=VECTOR_DIM,
    alpha=0.95,  # mostly semantic, with lexical correction
    vector_normalized=True,
    M=48,
    ef_construction=1024,
    ef_search=512,
)

engine.init(mode="build")

engine.ingest(
    external_id="p1",
    title="Apple iPhone 15 Pro Max 256GB Natural Titanium",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Apple",
        "storage": "256GB",
        "color": "Natural Titanium",
    },
    vector=np.random.randn(VECTOR_DIM).astype("float32"),
    normalize=True,
)

engine.ingest(
    external_id="p2",
    title="Samsung Galaxy S24 Ultra 512GB Black",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Samsung",
        "storage": "512GB",
        "color": "Black",
    },
    vector=np.random.randn(VECTOR_DIM).astype("float32"),
    normalize=True,
)

engine.finalize()

query_vector = np.random.randn(VECTOR_DIM).astype("float32")

results = engine.search(
    "iphone 15 pro max",
    category="Electronics",
    subcategory="Smartphones",
    attributes={
        "brand": "Apple",
    },
    vector=query_vector,
    normalize=True,
    k=10,
)

print(results)
```

To return distances:

```python
print(engine.search_with_distance("iphone 15", k=10))
```

---

### Understanding `alpha`

`alpha` controls the balance between semantic vector similarity and structured lexical matching.

| `alpha` | Behavior |
|---:|---|
| `0.0` | lexical-only |
| `0.5` | balanced lexical + semantic |
| `0.95` | mostly semantic, with lexical correction |
| `1.0` | semantic-only |

For semantic-only and hybrid search, pass `vector_dim` during engine construction and provide vectors during `ingest(...)` and `search(...)`.

Choose `alpha` before building the index. In brinicle, `alpha` affects graph construction as well as search scoring; it is not only a query-time reranking parameter.

---

## Autocomplete

`AutocompleteEngine` provides low-RAM autocomplete and query suggestion search using brinicle's HNSW infrastructure.

It can be used to index:

- popular queries
- item titles
- category names
- curated suggestions

```python
import brinicle

ac = brinicle.AutocompleteEngine(
    "autocomplete_index",
    dim=48,
)

ac.init(mode="build")

ac.ingest("iphone 15 pro max", "iphone 15 pro max")
ac.ingest("iphone 15 case", "iphone 15 case")
ac.ingest("samsung s24 ultra", "samsung s24 ultra")

ac.finalize()

print(ac.search("iph", k=5))
```

Autocomplete currently works best for prefix-aligned query and title suggestions.

---

## Streaming-first ingest

brinicle ingests records one at a time, so the full dataset does not need to fit in memory.

```python
client.init(mode="build")

for item in stream_items():
    client.ingest(...)

client.finalize()
```

---

## Configuration

brinicle exposes common HNSW parameters:

- `M`
- `ef_construction`
- `ef_search`
- `delta_ratio`

Example:

```python
engine = brinicle.VectorEngine(
    "vector_index",
    dim=384,
    M=48,
    ef_construction=1024,
    ef_search=512,
    delta_ratio=0.1,
)
```

Item search supports alpha-controlled lexical, semantic, and hybrid scoring.

```python
engine = brinicle.ItemSearchEngine(
    "item_index",
    dim=96,
    vector_dim=384,
    alpha=0.95,
)
```

Advanced users can pass a custom `LexicalConfig`.

```python
cfg = brinicle.LexicalConfig()

cfg.search_title_weight = 0.60
cfg.search_category_weight = 0.15
cfg.search_subcategory_weight = 0.15
cfg.search_attr_weight = 0.1
cfg.build_title_weight = 0.6
cfg.build_category_weight = 0.15
cfg.build_subcategory_weight = 0.15
cfg.build_attr_weight = 0.1

engine = brinicle.ItemSearchEngine(
    "item_index",
    dim=96,
    lexical_config=cfg,
)
```

Autocomplete also supports its own scoring configuration.

```python
cfg = brinicle.AutocompleteConfig()

cfg.search_position_decay = 0.5
cfg.search_length_penalty = 0.2

ac = brinicle.AutocompleteEngine(
    "autocomplete_index",
    dim=48,
    autocomplete_config=cfg,
)
```

---

## Index files

For an index path such as:

```python
engine = brinicle.VectorEngine("my_index", dim=128)
```

brinicle stores index files beside that base path:

```text
my_index.main
my_index.delta
my_index.lock
```

High-level engines such as `ItemSearchEngine` and `AutocompleteEngine` may also store tokenizer and encoding metadata beside the index.

---

## Which engine should I use?

Use `VectorEngine` for raw ANN search over embeddings or numeric vectors.

Use `ItemSearchEngine` for catalog-like records with titles, metadata, and optional semantic vectors:

- `alpha=0.0` for lexical-only search
- `alpha=1.0` for semantic-only search
- `0.0 < alpha < 1.0` for hybrid search

Use `AutocompleteEngine` for low-RAM query or title suggestions.

---

## License

brinicle is licensed under the Apache License, Version 2.0.

See the [LICENSE](https://github.com/bicardinal/brinicle/blob/main/LICENSE) file.
