# K-nearest neighbor vector search

Manticore Search supports the ability to add embeddings generated by Machine Learning models to each document, and then doing a nearest-neighbor search on them. This lets you build features like similarity search, recommendations, semantic search, and relevance ranking based on NLP algorithms, among others, including image, video, and sound searches.

To combine KNN vector search with full-text search for better relevance, see [Hybrid search](../Searching/Hybrid_search.md).

## What is an embedding?

An embedding is a method of representing data - such as text, images, or sound - as vectors in a high-dimensional space. These vectors are crafted to ensure that the distance between them reflects the similarity of the data they represent. This process typically employs algorithms like word embeddings (e.g., Word2Vec, BERT) for text or neural networks for images. The high-dimensional nature of the vector space, with many components per vector, allows for the representation of complex and nuanced relationships between items. Their similarity is gauged by the distance between these vectors, often measured using methods like Euclidean distance or cosine similarity.

Manticore Search enables k-nearest neighbor (KNN) vector searches using the HNSW library. This functionality is part of the [Manticore Columnar Library](https://github.com/manticoresoftware/columnar).

<!-- example KNN -->

### Configuring a table for KNN search

To run KNN searches, you must first configure your table. Float vectors and KNN search are only supported in real-time tables (not in plain tables). The table needs to have at least one [float_vector](../Creating_a_table/Data_types.md#Float-vector) attribute, which serves as a data vector. You need to specify the following properties:
* `knn_type`: A mandatory setting; currently, only `hnsw` is supported.
* `knn_dims`: A mandatory setting that specifies the dimensions of the vectors being indexed.
* `hnsw_similarity`: A mandatory setting that specifies the distance function used by the HNSW index. Acceptable values are:
  - `L2` - Squared L2
  - `IP` - Inner product
  - `COSINE` - Cosine similarity

  **Note:** When using `COSINE` similarity, vectors are automatically normalized upon insertion. This means the stored vector values may differ from the original input values, as they will be converted to unit vectors (vectors with a mathematical length/magnitude of 1.0) to enable efficient cosine similarity calculations. This normalization preserves the direction of the vector while standardizing its length.
* `hnsw_m`: An optional setting that defines the maximum number of outgoing connections in the graph. The default is 16.
* `hnsw_ef_construction`: An optional setting that defines a construction time/accuracy trade-off. The default is 200.

> NOTE: HNSW graph construction during RT chunk saves, `OPTIMIZE TABLE` / auto-optimize chunk merges, and `ALTER TABLE ... ADD/DROP/REBUILD` KNN rebuilds runs in parallel by default on multi-core hosts; the worker count is controlled by the [`knn_parallel_build`](../Server_settings/Searchd.md#knn_parallel_build) searchd setting (set it to `1` to force the serial path). This affects build-time performance only. Because parallel HNSW construction may insert vectors in a different order, the resulting graph may not be bit-identical to a serial build.

<!-- intro -->
##### SQL

<!-- request SQL -->
```sql
create table test ( title text, image_vector float_vector knn_type='hnsw' knn_dims='4' hnsw_similarity='l2' );
```

<!-- response SQL -->

```sql
Query OK, 0 rows affected (0.01 sec)
```

<!-- request JSON -->
```json
POST /sql?mode=raw -d "create table test ( title text, image_vector float_vector knn_type='hnsw' knn_dims='4' hnsw_similarity='l2' )"
```

<!-- response JSON -->

```json
[
  {
    "total": 0,
    "error": "",
    "warning": ""
  }
]
```

<!-- intro -->
##### Plain mode (using configuration file) - Manual vectors:

<!-- request Config -->
```ini
table test_vec {
    type = rt
	...
    rt_attr_float_vector = image_vector
    knn = {"attrs":[{"name":"image_vector","type":"hnsw","dims":4,"hnsw_similarity":"L2","hnsw_m":16,"hnsw_ef_construction":200}]}
}
```

**Note:** For auto-embeddings in plain mode, see the example below, which shows how to use `model_name` and `from` parameters in the `knn` configuration.

<!-- end -->

<!-- example knn_insert -->

### Inserting vector data

#### Auto Embeddings (Recommended)

The easiest way to work with vector data is using **auto embeddings**. With this feature, you create a table with `MODEL_NAME` and `FROM` parameters, then simply insert your text data - Manticore automatically generates embeddings for you.

##### Creating a table with auto embeddings

When creating a table for auto embeddings, specify:
- `MODEL_NAME`: The embedding model to use
- `FROM`: Which fields to use for embedding generation (empty means all text/string fields)
- `API_KEY`: Required for remote models (OpenAI, Voyage, Jina). The API key is validated during table creation by making a real API request.
- `API_URL`: Optional. Custom API endpoint URL. If not specified, uses the default provider endpoint (e.g., `https://api.openai.com/v1/embeddings` for OpenAI).
- `API_TIMEOUT`: Optional. HTTP timeout in seconds for API requests. Default is 10 seconds. Set to `'0'` to use the default timeout. Applies to both validation requests during table creation and embedding generation during INSERT operations.

For remote models, `MODEL_NAME` can be written in two forms:
- Legacy provider-prefixed form: `openai/text-embedding-ada-002`, `voyage/voyage-3.5-lite`, `jina/jina-embeddings-v4`
- Explicit provider-signal form for custom endpoints: `openai:text-embedding-ada-002`, `openai:openai/text-embedding-ada-002`, `voyage:custom-model`, `jina:custom-model`

When you use the `provider:model` form together with `API_URL`, the part before `:` only selects the request format. The part after `:` is sent to the remote endpoint unchanged. This is useful for OpenAI-compatible gateways such as OpenRouter or LiteLLM.

**Supported embedding models:**

| Model Type | Example | API Key Required | Notes |
|------------|---------|-----------------|-------|
| **ONNX (recommended)** | `Xenova/all-MiniLM-L6-v2` | No | Local models from any Hugging Face repo that ships an `.onnx` file. Runs on Manticore's fast ONNX Runtime backend. Browse the list: [feature-extraction ONNX models](https://huggingface.co/Xenova/models?pipeline_tag=feature-extraction&search=minilm). |
| **Sentence Transformers** | `sentence-transformers/all-MiniLM-L6-v2` | No | Local BERT-based models, auto-downloaded. Still supported — use ONNX above when available. |
| **Qwen** | `Qwen/Qwen3-Embedding-0.6B` | No | Local Qwen family models |
| **Llama** | `TinyLlama/TinyLlama-1.1B-Chat-v1.0` | No | Local Llama family models |
| **Mistral** | `Locutusque/TinyMistral-248M-v2` | No | Local Mistral family models |
| **Gemma** | `h2oai/embeddinggemma-300m` | No | Local Gemma family models |
| **OpenAI** | `openai/text-embedding-ada-002` or `openai:text-embedding-ada-002` | Yes | `API_KEY='***'` |
| **Voyage** | `voyage/voyage-3.5-lite` or `voyage:voyage-3.5-lite` | Yes | `API_KEY='***'` |
| **Jina** | `jina/jina-embeddings-v4` or `jina:jina-embeddings-v4` | Yes | `API_KEY='***'` |

**Local model format requirements:**
- Must be saved in `safetensors` format (single-file only)
- Supported families: Qwen, Llama, Mistral, Gemma
- Tested models: `TinyLlama/TinyLlama-1.1B-Chat-v1.0`, `Locutusque/TinyMistral-248M-v2`, `Qwen/Qwen3-Embedding-0.6B`, `h2oai/embeddinggemma-300m`
- Other `safetensors` models may also work, but are not guaranteed

More information about setting up a `float_vector` attribute can be found [here](../Creating_a_table/Data_types.md#Float-vector).

<!-- intro -->
##### SQL:

<!-- request SQL -->

Using a local [ONNX model](https://huggingface.co/Xenova/models?pipeline_tag=feature-extraction&search=minilm) — recommended (no API key needed)
```sql
CREATE TABLE products (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='Xenova/all-MiniLM-L6-v2' FROM='title'
);
```

Using sentence-transformers (no API key needed; runs on the Candle path — use ONNX above when available)
```sql
CREATE TABLE products_st (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='sentence-transformers/all-MiniLM-L6-v2' FROM='title'
);
```

Using Qwen local embeddings (no API key needed)
```sql
CREATE TABLE products_qwen (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='Qwen/Qwen3-Embedding-0.6B' FROM='title' CACHE_PATH='/opt/homebrew/var/manticore/.cache/manticore'
);
```

Using OpenAI (requires API_KEY parameter)
```sql
CREATE TABLE products_openai (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='openai/text-embedding-ada-002' FROM='title,description' API_KEY='...'
);
```
Using OpenAI with custom API URL and timeout (optional)
```sql
CREATE TABLE products_openai_custom (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='openai:text-embedding-ada-002' FROM='title,description'
    API_KEY='***' API_URL='https://custom-api.example.com/v1/embeddings' API_TIMEOUT='30'
);
```

Using an OpenAI-compatible gateway that expects a provider-qualified model ID
```sql
CREATE TABLE products_openrouter (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='openai:openai/text-embedding-ada-002' FROM='title,description'
    API_KEY='***' API_URL='https://openrouter.ai/api/v1/embeddings' API_TIMEOUT='30'
);
```

Using all text fields for embeddings (FROM is empty)
```sql
CREATE TABLE products_all (
    title TEXT,
    description TEXT,
    embedding_vector FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2'
    MODEL_NAME='Xenova/all-MiniLM-L6-v2' FROM=''
);
```

<!-- intro -->
##### Plain mode (using configuration file):

<!-- request Config -->
```ini
table products {
    type = rt
    path = /path/to/products
    rt_field = title
    rt_field = description
    rt_attr_float_vector = embedding_vector
    knn = {"attrs":[{"name":"embedding_vector","type":"hnsw","hnsw_similarity":"L2","hnsw_m":16,"hnsw_ef_construction":200,"model_name":"Xenova/all-MiniLM-L6-v2","from":"title"}]}
}
```

Using OpenAI with API key in plain mode:
```ini
table products_openai {
    type = rt
    path = /path/to/products_openai
    rt_field = title
    rt_field = description
    rt_attr_float_vector = embedding_vector
    knn = {"attrs":[{"name":"embedding_vector","type":"hnsw","hnsw_similarity":"L2","hnsw_m":16,"hnsw_ef_construction":200,"model_name":"openai/text-embedding-ada-002","from":"title,description","api_key":"your-api-key-here"}]}
}
```

Using all text fields (empty FROM):
```ini
table products_all {
    type = rt
    path = /path/to/products_all
    rt_field = title
    rt_field = description
    rt_attr_float_vector = embedding_vector
    knn = {"attrs":[{"name":"embedding_vector","type":"hnsw","hnsw_similarity":"L2","hnsw_m":16,"hnsw_ef_construction":200,"model_name":"Xenova/all-MiniLM-L6-v2","from":""}]}
}
```

**Important notes for plain mode:**
- When using `model_name`, you **must not** specify `dims` - the model automatically determines the vector dimensions. The `dims` and `model_name` parameters are mutually exclusive.
- When **not** using `model_name` (manual vector insertion), you **must** specify `dims` to indicate the vector dimensions.
- The `from` parameter specifies which fields to use for embedding generation (comma-separated list, or empty string for all text/string fields). This parameter is required when using `model_name`.
- For API-based models (OpenAI, Voyage, Jina), include the `api_key` parameter in the knn configuration

<!-- end -->

##### Inserting data with auto embeddings

<!--
data for the following example:

DROP TABLE IF EXISTS products;
CREATE TABLE products(title text, embedding_vector float_vector knn_type='hnsw' hnsw_similarity='l2' model_name='Xenova/all-MiniLM-L6-v2' from='title');
DROP TABLE IF EXISTS products_openai;
CREATE TABLE products_openai(title text, description text, embedding_vector float_vector knn_type='hnsw' hnsw_similarity='l2' model_name='Xenova/all-MiniLM-L6-v2' from='title,description');
-->

<!-- example inserting_embeddings -->

When using auto embeddings, you can:

- Omit the vector field and let Manticore generate embeddings from the fields listed in `FROM`
- Provide your own vector explicitly for a row
- Provide `()` to skip generation and store an all-zero vector

If you later run `ALTER TABLE ... REBUILD EMBEDDINGS`, rows that currently contain zero vectors from `()` are regenerated too.

<!-- intro -->
##### SQL:

<!-- request SQL -->

Insert text data only - embeddings generated automatically
```sql
INSERT INTO products (title) VALUES
('machine learning artificial intelligence'),
('banana fruit sweet yellow');
```

Insert a user-provided vector
```sql
INSERT INTO products (title, embedding_vector) VALUES
('machine learning artificial intelligence', (0.653448,0.192478,0.017971,0.339821));
```

Insert multiple fields - both used for embedding if FROM='title,description'
```sql
INSERT INTO products_openai (title, description) VALUES
('smartphone', 'latest mobile device with advanced features'),
('laptop', 'portable computer for work and gaming');
```

Insert empty vector (no auto generation; stores a zero vector)
```sql
INSERT INTO products (title, embedding_vector) VALUES
('no embedding item', ());
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

Insert text data only - embeddings generated automatically
```JSON
POST /sql?mode=raw -d "INSERT INTO products (title) VALUES ('machine learning artificial intelligence'),('banana fruit sweet yellow')"
```

Insert multiple fields - both used for embedding if FROM='title,description'  
```JSON
POST /sql?mode=raw -d "INSERT INTO products_openai (title, description) VALUES ('smartphone', 'latest mobile device with advanced features'), ('laptop', 'portable computer for work and gaming')"
```

Insert empty vector (document excluded from vector search)
```JSON
POST /sql?mode=raw -d "INSERT INTO products (title, embedding_vector) VALUES ('no embedding item', ())"
```

<!-- end -->

##### Searching with auto embeddings

<!-- example embeddings_search -->
Search works the same way - provide your query text and Manticore will generate embeddings and find similar documents:

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
SELECT id, knn_dist() FROM products WHERE knn(embedding_vector, 'machine learning');
```

<!-- response SQL -->

```sql
+------+------------+
| id   | knn_dist() |
+------+------------+
|    1 | 0.12345678 |
|    2 | 0.87654321 |
+------+------------+
2 rows in set (0.00 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

Using text query with auto-embeddings
```json
POST /search
{
    "table": "products",
    "knn": {
        "field": "embedding_vector",
        "query": "machine learning"
    }
}
```

Using vector query directly
```json
POST /search
{
    "table": "products",
    "knn": {
        "field": "embedding_vector",
        "query": [0.1, 0.2, 0.3, 0.4]
    }
}
```

<!-- response JSON -->

```json
{
    "took": 0,
    "timed_out": false,
    "hits": {
        "total": 2,
        "total_relation": "eq",
        "hits": [
            {
                "_id": 1,
                "_score": 1,
                "_knn_dist": 0.12345678,
                "_source": {
                    "title": "machine learning artificial intelligence"
                }
            },
            {
                "_id": 2,
                "_score": 1,
                "_knn_dist": 0.87654321,
                "_source": {
                    "title": "banana fruit sweet yellow"
                }
            }
        ]
    }
}
```

<!-- end -->

#### Manual Vector Insertion

<!-- example manual_vector -->
Alternatively, you can manually insert pre-computed vector data, ensuring it matches the dimensions you specified when creating the table. You can also insert an empty vector; this means that the document will be excluded from vector search results.

**Important:** When using `hnsw_similarity='cosine'`, vectors are automatically normalized upon insertion to unit vectors (vectors with a mathematical length/magnitude of 1.0). This normalization preserves the direction of the vector while standardizing its length, which is required for efficient cosine similarity calculations. This means the stored values will differ from your original input values.

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
insert into test values ( 1, 'yellow bag', (0.653448,0.192478,0.017971,0.339821) ), ( 2, 'white bag', (-0.148894,0.748278,0.091892,-0.095406) );
```
<!-- response SQL -->

```sql
Query OK, 2 rows affected (0.00 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
POST /insert
{
	"table":"test_vec",
	"id":1,
	"doc": 	{ "title" : "yellow bag", "image_vector" : [0.653448,0.192478,0.017971,0.339821] }
}

POST /insert
{
	"table":"test_vec",
	"id":2,
	"doc": 	{ "title" : "white bag", "image_vector" : [-0.148894,0.748278,0.091892,-0.095406] }
}
```

<!-- response JSON -->

```json
{
	"table":"test",
	"id":1,
	"created":true,
	"result":"created",
	"status":201
}

{
	"table":"test",
	"id":2,
	"created":true,
	"result":"created",
	"status":201
}
```

<!-- end -->

<!-- example knn_search -->

### KNN vector search

Now, you can perform a KNN search using the `knn` clause in either SQL or JSON format. Both interfaces support the same essential parameters, ensuring a consistent experience regardless of the format you choose:

- SQL: `select ... from <table name> where knn ( <field>, <query vector> [,<options>] )`
- JSON:
  ```
  POST /search
  {
      "table": "<table name>",
      "knn":
      {
          "field": "<field>",
          "query": "<text or vector>",
          "ef": <ef>,
		  "rescore": <rescore>,
		  "oversampling": <oversampling>
      }
  }
  ```

The parameters are:
* `field`: This is the name of the float vector attribute containing vector data.
* `k`: Deprecated option. Use query `limit` instead. It was used to specify the quantity of documents that a single HNSW index should return. However, the actual number of documents included in the final results may vary. For instance, if the system is dealing with real-time tables divided into disk chunks, each chunk could return `k` documents, leading to a total that exceeds the specified `k` (as the cumulative count would be `num_chunks * k`). On the other hand, the final document count might be less than `k` if, after requesting `k` documents, some are filtered out based on specific attributes. It's important to note that the parameter `k` does not apply to ramchunks. In the context of ramchunks, the retrieval process operates differently, and thus, the `k` parameter's effect on the number of documents returned is not applicable.
* `query`: (Recommended parameter) The search query, which can be either:
  - Text string: Automatically converted to embeddings if the field has auto-embeddings configured. Will return an error if the field doesn't have auto-embeddings.
  - Vector array: Works the same as `query_vector`.
* `query_vector`: (Legacy parameter) The search vector as an array of numbers. Still supported for backward compatibility.
  **Note:** Use either `query` or `query_vector`, not both in the same request.
* `ef`: optional size of the dynamic list used during the search. A higher `ef` leads to more accurate but slower search. The default is 10.
* `rescore`: Enables KNN rescoring (enabled by default). Set to `0` in SQL or `false` in JSON to disable rescoring. After the KNN search is completed using quantized vectors (with possible oversampling), distances are recalculated with the original (full-precision) vectors and results are re-sorted to improve ranking accuracy.
* `oversampling`: Sets a factor (float value) by which `k` is multiplied when executing the KNN search, causing more candidates to be retrieved than needed using quantized vectors. `oversampling=3.0` is applied by default. These candidates can be re-evaluated later if rescoring is enabled. Oversampling also works with non-quantized vectors. Since it increases `k`, which affects how the HNSW index works, it may cause a small change in result accuracy.
* `early_termination`: Enables or disables adaptive early termination during HNSW graph traversal. Enabled by default. Set to `0` in SQL or `false` in JSON to disable. See [Early termination](../Searching/KNN.md#Early-termination) for details.

When a text query is supplied (so Manticore embeds the string before the search), the number of threads used by the embeddings library can be overridden per-query in SQL with `OPTION embeddings_threads = N`. The value caps the embeddings call for this query only, overriding the global [embeddings_threads](../Server_settings/Searchd.md#embeddings_threads) setting; `0` means uncapped. The option has no effect when the query is supplied as a vector array.

Documents are always sorted by their distance to the search vector. Any additional sorting criteria you specify will be applied after this primary sort condition. For retrieving the distance, there is a built-in function called [knn_dist()](../Functions/Other_functions.md#KNN_DIST%28%29).

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
select id, knn_dist() from test where knn ( image_vector, (0.286569,-0.031816,0.066684,0.032926), { ef=2000, oversampling=3.0, rescore=1 } );
```
<!-- response SQL -->

```sql
+------+------------+
| id   | knn_dist() |
+------+------------+
|    1 | 0.28146550 |
|    2 | 0.81527930 |
+------+------------+
2 rows in set (0.00 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
POST /search
{
	"table": "test",
	"knn":
	{
		"field": "image_vector",
		"query": [0.286569,-0.031816,0.066684,0.032926],
		"ef": 2000,
		"rescore": true,
		"oversampling": 3.0
	}
}
```

<!-- response JSON -->

```json
{
	"took":0,
	"timed_out":false,
	"hits":
	{
		"total":2,
		"total_relation":"eq",
		"hits":
		[
			{
				"_id": 1,
				"_score":1,
				"_knn_dist":0.28146550,
				"_source":
				{
					"title":"yellow bag",
					"image_vector":[0.653448,0.192478,0.017971,0.339821]
				}
			},
			{
				"_id": 2,
				"_score":1,
				"_knn_dist":0.81527930,
				"_source":
				{
					"title":"white bag",
					"image_vector":[-0.148894,0.748278,0.091892,-0.095406]
				}
			}
		]
	}
}
```

<!-- end -->

<!-- example knn_quantization -->

### Vector quantization

HNSW indexes need to be fully loaded into memory to perform KNN search, which can lead to significant memory consumption. To reduce memory usage, scalar quantization can be applied - a technique that compresses high-dimensional vectors by representing each component (dimension) with a limited number of discrete values. Manticore supports 8-bit and 1-bit quantization, meaning each vector component is compressed from a 32-bit float to 8 bits or even 1 bit, reducing memory usage by 4x or 32x, respectively. These compressed representations also allow for faster distance calculations, as more vector components can be processed in a single SIMD instruction. Although scalar quantization introduces some approximation error, it is often a worthwhile trade-off between search accuracy and resource efficiency. For even better accuracy, quantization can be combined with rescoring and oversampling: more candidates are retrieved than requested, and distances for these candidates are recalculated using the original 32-bit float vectors.

Supported quantization types include:
* `8bit`: Each vector component is quantized to 8 bits.
* `1bit`: Each vector component is quantized to 1 bit. Asymmetric quantization is used, with query vectors quantized to 4 bits and stored vectors to 1 bit. This approach offers greater precision than simpler methods, though with some performance trade-off.
* `1bitsimple`: Each vector component is quantized to 1 bit. This method is faster than `1bit`, but typically less accurate.

<!-- intro -->
##### SQL:

<!-- request SQL -->
```sql
create table test ( title text, image_vector float_vector knn_type='hnsw' knn_dims='4' hnsw_similarity='l2' quantization='1bit');
```

<!-- response SQL -->

```sql
Query OK, 0 rows affected (0.01 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->
```json
POST /sql?mode=raw -d "create table test ( title text, image_vector float_vector knn_type='hnsw' knn_dims='4' hnsw_similarity='l2' quantization='1bit')"
```

<!-- response JSON -->

```json
[
  {
    "total": 0,
    "error": "",
    "warning": ""
  }
]
```

<!-- end -->

<!-- Example knn_similar_docs -->

### Find similar docs by id

> NOTE: Finding similar documents by id requires [Manticore Buddy](../Installation/Manticore_Buddy.md). If it doesn't work, make sure Buddy is installed.

Finding documents similar to a specific one based on its unique ID is a common task. For instance, when a user views a particular item, Manticore Search can efficiently identify and display a list of items that are most similar to it in the vector space. Here's how you can do it:

- SQL: `select ... from <table name> where knn ( <field>, <k>, <document id> )`
- JSON:
  ```
  POST /search
  {
      "table": "<table name>",
      "knn":
      {
          "field": "<field>",
          "doc_id": <document id>,
          "k": <k>
      }
  }
  ```

The parameters are:
* `field`: This is the name of the float vector attribute containing vector data.
* `k`: This represents the number of documents to return and is a key parameter for Hierarchical Navigable Small World (HNSW) indexes. It specifies the quantity of documents that a single HNSW index should return. However, the actual number of documents included in the final results may vary. For instance, if the system is dealing with real-time tables divided into disk chunks, each chunk could return `k` documents, leading to a total that exceeds the specified `k` (as the cumulative count would be `num_chunks * k`). On the other hand, the final document count might be less than `k` if, after requesting `k` documents, some are filtered out based on specific attributes. It's important to note that the parameter `k` does not apply to ramchunks. In the context of ramchunks, the retrieval process operates differently, and thus, the `k` parameter's effect on the number of documents returned is not applicable.
* `document id`: Document ID for KNN similarity search.


<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
select id, knn_dist() from test where knn ( image_vector, 5, 1 );
```
<!-- response SQL -->

```sql
+------+------------+
| id   | knn_dist() |
+------+------------+
|    2 | 0.81527930 |
+------+------------+
1 row in set (0.00 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
POST /search
{
  "table": "test",
  "knn":
  {
    "field": "image_vector",
    "doc_id": 1,
    "k": 5
  }
}
```

<!-- response JSON -->

```json
{
	"took":0,
	"timed_out":false,
	"hits":
	{
		"total":1,
		"total_relation":"eq",
		"hits":
		[
			{
				"_id": 2,
				"_score":1643,
				"_knn_dist":0.81527930,
				"_source":
				{
					"title":"white bag",
					"image_vector":[-0.148894,0.748278,0.091892,-0.095406]
				}
			}
		]
	}
}
```

<!-- end -->

<!-- Example knn_filtering -->

### Filtering KNN vector search results

Manticore also supports additional filtering of documents returned by the KNN search, either by full-text matching, attribute filters, or both.

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
select id, knn_dist() from test where knn ( image_vector, 5, (0.286569,-0.031816,0.066684,0.032926) ) and match('white') and id < 10;
```
<!-- response SQL -->

```sql
+------+------------+
| id   | knn_dist() |
+------+------------+
|    2 | 0.81527930 |
+------+------------+
1 row in set (0.00 sec)
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
POST /search
{
	"table": "test",
	"knn":
	{
		"field": "image_vector",
		"query": [0.286569,-0.031816,0.066684,0.032926],
		"k": 5
	},
	"query":
	{
		"bool":
		{
			"must":
			[
				{ "match": {"_all":"white"} },
				{ "range": { "id": { "lt": 10 } } }
			]
		}
	}
}
```

<!-- response JSON -->

```json
{
	"took":0,
	"timed_out":false,
	"hits":
	{
		"total":1,
		"total_relation":"eq",
		"hits":
		[
			{
				"_id": 2,
				"_score":1643,
				"_knn_dist":0.81527930,
				"_source":
				{
					"title":"white bag",
					"image_vector":[-0.148894,0.748278,0.091892,-0.095406]
				}
			}
		]
	}
}
```

<!-- end -->

<!-- example knn_filtering_strategies -->

### Filtering strategies: prefilter vs. postfilter

When combining KNN vector search with attribute filters, Manticore supports two strategies that differ in when the filter is applied relative to HNSW graph traversal.

* Prefiltering (default; `prefilter=1` (SQL) or `"prefilter": true` (JSON, default)) passes the filter into the HNSW traversal itself. Each candidate is checked against the filter before being added to the result heap - only matching documents contribute to the final `k` results. This reduces wasted distance computations and guarantees that exactly `k` matching documents are returned (assuming `k` matching documents exist).

* Postfiltering (`prefilter=0` (SQL) or `"prefilter": false` (JSON)) runs the KNN search first over the full dataset, then applies the filter to the results. This is safe and predictable: the HNSW graph is traversed without interference, and the filter only affects which results are returned to the client. The downside is that the graph may spend effort on candidates that will ultimately be discarded. With a tight filter that matches only a small fraction of documents, the returned `k` results may be significantly fewer than requested, because most KNN candidates fail the filter.

Internally, Manticore uses an ACORN-1-based algorithm for prefiltering. A naive prefilter would simply skip non-matching nodes, which risks losing "bridge" nodes that connect otherwise-separated parts of the HNSW graph, causing recall to collapse as the filter becomes more selective. ACORN-1 avoids this: when a node fails the filter, its neighbors are still added to the exploration queue. This allows the traversal to route around filtered-out nodes and maintain graph connectivity. ACORN-1 exploration is activated automatically when fewer than 60% of the total documents pass the filter.

**Automatic brute-force fallback:** When prefiltering is enabled, Manticore estimates whether it is cheaper to run a brute-force distance scan over the filtered subset than to traverse the HNSW graph. The estimate compares the expected number of nodes visited by HNSW against the number of documents that pass the filter. If the filtered set is small enough that scanning it directly is faster, Manticore automatically switches to brute-force, skipping HNSW entirely. This ensures correctness and good performance even under extreme selectivity.

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
-- prefilter (default): filter applied during HNSW traversal (ACORN-1 used automatically)
SELECT id, knn_dist() FROM test
WHERE knn ( image_vector, (0.286569,-0.031816,0.066684,0.032926) )
AND price < 100;

-- postfilter: KNN runs over full dataset, filter applied to results
SELECT id, knn_dist() FROM test
WHERE knn ( image_vector, (0.286569,-0.031816,0.066684,0.032926), { prefilter=0 } )
AND price < 100;
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
// prefilter (default): filter applied during HNSW traversal
POST /search
{
    "table": "test",
    "knn": {
        "field": "image_vector",
        "query": [0.286569,-0.031816,0.066684,0.032926]
    },
    "query": {
        "range": { "price": { "lt": 100 } }
    }
}

// postfilter: filter applied after KNN search
POST /search
{
    "table": "test",
    "knn": {
        "field": "image_vector",
        "query": [0.286569,-0.031816,0.066684,0.032926],
        "prefilter": false
    },
    "query": {
        "range": { "price": { "lt": 100 } }
    }
}
```

<!-- end -->

### Early termination

By default, Manticore uses an adaptive early termination algorithm during HNSW graph traversal. Instead of always exploring the full candidate set defined by `ef`, it monitors the rate at which new candidates improve the result set and stops early when that rate consistently falls below a threshold. This reduces the number of distance computations without significantly affecting result quality.

Early termination is enabled by default and is automatically disabled when `k` is 10 or fewer, since the overhead of the algorithm is not worthwhile for such small result sets. The performance benefit scales with `k` — the larger the result set, the more distance computations can be saved by stopping early.

Note that oversampling multiplies the effective `k` used during HNSW traversal, so early termination also benefits from oversampling: a higher effective `k` means more candidates to potentially skip.

<!-- example knn_early_termination -->
To explicitly control early termination, use the `early_termination` option:

<!-- intro -->
##### SQL:

<!-- request SQL -->

```sql
-- disable early termination
SELECT id, knn_dist() FROM test WHERE knn ( image_vector, (0.286569,-0.031816,0.066684,0.032926), { ef=200, early_termination=0 } );

-- enable early termination explicitly (default)
SELECT id, knn_dist() FROM test WHERE knn ( image_vector, (0.286569,-0.031816,0.066684,0.032926), { ef=200, early_termination=1 } );
```

<!-- intro -->
##### JSON:

<!-- request JSON -->

```json
POST /search
{
    "table": "test",
    "knn":
    {
        "field": "image_vector",
        "query": [0.286569,-0.031816,0.066684,0.032926],
        "ef": 200,
        "early_termination": false
    }
}
```

<!-- end -->

When to disable early termination:
* When result set precision is critical and you cannot afford any approximation beyond what HNSW already provides.
* When using low `k` values (around 30 or fewer), where early termination provides little performance benefit but may reduce precision.

<!-- proofread -->
