From 5572a7479ce9462bebc17d73812cb90af07882e8 Mon Sep 17 00:00:00 2001 From: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:57:53 -0400 Subject: [PATCH] Add k-NN Faiss filtering documentation (#4476) * Add k-NN Faiss filtering documentation Signed-off-by: Fanit Kolchina * Move the note Signed-off-by: Fanit Kolchina * Add faiss and a filter table Signed-off-by: Fanit Kolchina * Refactor boolean filtering section Signed-off-by: Fanit Kolchina * Clarified that Faiss works with hnsw only Signed-off-by: Fanit Kolchina * Add more Faiss filtering information Signed-off-by: Fanit Kolchina * Apply suggestions from code review Co-authored-by: Melissa Vagi Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Nathan Bower Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> * Update _search-plugins/knn/filter-search-knn.md Co-authored-by: Nathan Bower Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> * Implemented editorial comments Signed-off-by: Fanit Kolchina * Implemented one more editorial comment Signed-off-by: Fanit Kolchina --------- Signed-off-by: Fanit Kolchina Signed-off-by: kolchfa-aws <105444904+kolchfa-aws@users.noreply.github.com> Co-authored-by: Melissa Vagi Co-authored-by: Nathan Bower --- _search-plugins/knn/approximate-knn.md | 24 +- _search-plugins/knn/filter-search-knn.md | 583 ++++++++++++++++------- images/faiss-algorithm.jpg | Bin 0 -> 26341 bytes 3 files changed, 409 insertions(+), 198 deletions(-) create mode 100644 images/faiss-algorithm.jpg diff --git a/_search-plugins/knn/approximate-knn.md b/_search-plugins/knn/approximate-knn.md index d57d164050..13505b046c 100644 --- a/_search-plugins/knn/approximate-knn.md +++ b/_search-plugins/knn/approximate-knn.md @@ -242,30 +242,8 @@ POST _bulk After data is ingested, it can be search just like any other `knn_vector` field! ### Using approximate k-NN with filters -If you use the `knn` query alongside filters or other clauses (e.g. `bool`, `must`, `match`), you might receive fewer than `k` results. In this example, `post_filter` reduces the number of results from 2 to 1: -```json -GET my-knn-index-1/_search -{ - "size": 2, - "query": { - "knn": { - "my_vector2": { - "vector": [2, 3, 5, 6], - "k": 2 - } - } - }, - "post_filter": { - "range": { - "price": { - "gte": 5, - "lte": 10 - } - } - } -} -``` +To learn about using filters with k-NN search, see [k-NN search with filters]({{site.url}}{{site.baseurl}}/search-plugins/knn/filter-search-knn/). ## Spaces diff --git a/_search-plugins/knn/filter-search-knn.md b/_search-plugins/knn/filter-search-knn.md index 2e886ad5b2..7e926a970e 100644 --- a/_search-plugins/knn/filter-search-knn.md +++ b/_search-plugins/knn/filter-search-knn.md @@ -11,12 +11,24 @@ has_math: true To refine k-NN results, you can filter a k-NN search using one of the following methods: -- [Scoring script filter](#scoring-script-filter): This approach involves pre-filtering a document set and then running an exact k-NN search on the filtered subset. It does not scale for large filtered subsets. +- [Efficient k-NN filtering](#efficient-k-nn-filtering): This approach applies filtering _during_ the k-NN search, as opposed to before or after the k-NN search, which ensures that `k` results are returned (if there are at least `k` results in total). This approach is supported by the following engines: + - Lucene engine with a Hierarchical Navigable Small World (HNSW) algorithm (k-NN plugin versions 2.4 and later) + - Faiss engine with an HNSW algorithm (k-NN plugin versions 2.9 or later) -- [Boolean filter](#boolean-filter-with-ann-search): This approach runs an [approximate nearest neighbor (ANN)]({{site.url}}{{site.baseurl}}/search-plugins/knn/approximate-knn) search and then applies a filter to the results. Because of post-filtering, it may return significantly fewer than `k` results for a restrictive filter. +- [Post-filtering](#post-filtering): Because it is performed after the k-NN search, this approach may return significantly fewer than `k` results for a restrictive filter. You can use the following two filtering strategies for this approach: + - [Boolean post-filter](#boolean-filter-with-ann-search): This approach runs an [approximate nearest neighbor (ANN)]({{site.url}}{{site.baseurl}}/search-plugins/knn/approximate-knn/) search and then applies a filter to the results. The two query parts are executed independently, and then the results are combined based on the query operator (`should`, `must`, and so on) provided in the query. + - [The `post_filter` parameter](#post-filter-parameter): This approach runs an [ANN]({{site.url}}{{site.baseurl}}/search-plugins/knn/approximate-knn/) search on the full dataset and then applies the filter to the k-NN results. -- [Lucene k-NN filter](#using-a-lucene-k-nn-filter): This approach applies filtering _during_ the k-NN search, as opposed to before or after the k-NN search, which ensures that `k` results are returned. You can only use this method with the Hierarchical Navigable Small World (HNSW) algorithm implemented by the Lucene search engine in k-NN plugin versions 2.4 and later. +- [Scoring script filter](#scoring-script-filter): This approach involves pre-filtering a document set and then running an exact k-NN search on the filtered subset. It may have high latency and does not scale when filtered subsets are large. +The following table summarizes the preceding filtering use cases. + +Filter | When the filter is applied | Type of search | Supported engines and methods | Where to place the `filter` clause +:--- | :--- | :--- | :--- +Efficient k-NN filtering | During search (a hybrid of pre- and post-filtering) | Approximate | - `lucene` (`hnsw`)
- `faiss` (`hnsw`) | Inside the k-NN query clause. +Boolean filter | After search (post-filtering) | Approximate | - `lucene`
- `nmslib`
- `faiss` | Outside the k-NN query clause. Must be a leaf clause. +The `post_filter` parameter | After search (post-filtering) | Approximate | - `lucene`
- `nmslib`
- `faiss` | Outside the k-NN query clause. +Scoring script filter | Before search (pre-filtering) | Exact | N/A | Inside the script score query clause. ## Filtered search optimization @@ -31,176 +43,17 @@ Once you've estimated the number of documents in your index, the restrictiveness | Number of documents in an index | Percentage of documents the filter returns | k | Filtering method to use for higher recall | Filtering method to use for lower latency | | :-- | :-- | :-- | :-- | :-- | | 10M | 2.5 | 100 | Scoring script | Scoring script | -| 10M | 38 | 100 | Lucene filter | Boolean filter | -| 10M | 80 | 100 | Scoring script | Lucene filter | -| 1M | 2.5 | 100 | Lucene filter | Scoring script | -| 1M | 38 | 100 | Lucene filter | Lucene filter/scoring script | -| 1M | 80 | 100 | Boolean filter | Lucene filter | - -## Scoring script filter - -A scoring script filter first filters the documents and then uses a brute-force exact k-NN search on the results. For example, the following query searches for hotels with a rating between 8 and 10, inclusive, that provide parking and then performs a k-NN search to return the 3 hotels that are closest to the specified `location`: - -```json -POST /hotels-index/_search -{ - "size": 3, - "query": { - "script_score": { - "query": { - "bool": { - "filter": { - "bool": { - "must": [ - { - "range": { - "rating": { - "gte": 8, - "lte": 10 - } - } - }, - { - "term": { - "parking": "true" - } - } - ] - } - } - } - }, - "script": { - "source": "knn_score", - "lang": "knn", - "params": { - "field": "location", - "query_value": [ - 5.0, - 4.0 - ], - "space_type": "l2" - } - } - } - } -} -``` -{% include copy-curl.html %} - -## Boolean filter with ANN search - -A Boolean filter consists of a Boolean query that contains a k-NN query and a filter. For example, the following query searches for hotels that are closest to the specified `location` and then filters the results to return hotels with a rating between 8 and 10, inclusive, that provide parking: +| 10M | 38 | 100 | Efficient k-NN filtering | Boolean filter | +| 10M | 80 | 100 | Scoring script | Efficient k-NN filtering | +| 1M | 2.5 | 100 | Efficient k-NN filtering | Scoring script | +| 1M | 38 | 100 | Efficient k-NN filtering | Efficient k-NN filtering/scoring script | +| 1M | 80 | 100 | Efficient k-NN filtering | Boolean filter | -```json -POST /hotels-index/_search -{ - "size": 3, - "query": { - "bool": { - "filter": { - "bool": { - "must": [ - { - "range": { - "rating": { - "gte": 8, - "lte": 10 - } - } - }, - { - "term": { - "parking": "true" - } - } - ] - } - }, - "must": [ - { - "knn": { - "location": { - "vector": [ - 5, - 4 - ], - "k": 20 - } - } - } - ] - } - } -} -``` - -The response includes documents containing the matching hotels: - -```json -{ - "took" : 95, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 5, - "relation" : "eq" - }, - "max_score" : 0.72992706, - "hits" : [ - { - "_index" : "hotels-index", - "_id" : "3", - "_score" : 0.72992706, - "_source" : { - "location" : [ - 4.9, - 3.4 - ], - "parking" : "true", - "rating" : 9 - } - }, - { - "_index" : "hotels-index", - "_id" : "6", - "_score" : 0.3012048, - "_source" : { - "location" : [ - 6.4, - 3.4 - ], - "parking" : "true", - "rating" : 9 - } - }, - { - "_index" : "hotels-index", - "_id" : "5", - "_score" : 0.24154587, - "_source" : { - "location" : [ - 3.3, - 4.5 - ], - "parking" : "true", - "rating" : 8 - } - } - ] - } -} -``` +## Efficient k-NN filtering -The location of the `filter` clause matters when it's used with a k-NN query clause. If the `filter` clause is outside the k-NN query clause, it must be a leaf clause. In this case, the filter is applied after the k-NN search and works exactly like the `post_filter` keyword. If the `filter` clause is within the k-NN query clause, it works as a hybrid of pre- and post-filtering (this option is only supported for the Lucene search engine). +You can perform efficient k-NN filtering with the `lucene` or `faiss` engines. -## Lucene k-NN filter implementation +### Lucene k-NN filter implementation k-NN plugin version 2.2 introduced support for running k-NN searches with the Lucene engine using HNSW graphs. Starting with version 2.4, which is based on Lucene version 9.4, you can use Lucene filters for k-NN searches. @@ -216,7 +69,7 @@ The following flow chart outlines the Lucene algorithm. For more information about the Lucene filtering implementation and the underlying `KnnVectorQuery`, see the [Apache Lucene documentation](https://issues.apache.org/jira/browse/LUCENE-10382). -## Using a Lucene k-NN filter +### Using a Lucene k-NN filter Consider a dataset that includes 12 documents containing hotel information. The following image shows all hotels on an xy coordinate plane by location. Additionally, the points for hotels that have a rating between 8 and 10, inclusive, are depicted with orange dots, and hotels that provide parking are depicted with green circles. The search point is colored in red: @@ -224,7 +77,7 @@ Consider a dataset that includes 12 documents containing hotel information. The In this example, you will create an index and search for the three hotels with high ratings and parking that are the closest to the search location. -### Step 1: Create a new index +**Step 1: Create a new index** Before you can run a k-NN search with a filter, you need to create an index with a `knn_vector` field. For this field, you need to specify `lucene` as the engine and `hnsw` as the `method` in the mapping. @@ -262,7 +115,7 @@ PUT /hotels-index ``` {% include copy-curl.html %} -### Step 2: Add data to your index +**Step 2: Add data to your index** Next, add data to your index. @@ -297,7 +150,7 @@ POST /_bulk ``` {% include copy-curl.html %} -### Step 3: Search your data with a filter +**Step 3: Search your data with a filter** Now you can create a k-NN search with filters. In the k-NN query clause, include the point of interest that is used to search for nearest neighbors, the number of nearest neighbors to return (`k`), and a filter with the restriction criteria. Depending on how restrictive you want your filter to be, you can add multiple query clauses to a single request. @@ -404,7 +257,195 @@ The response returns the three hotels that are nearest to the search point and h } ``` -Note that there are multiple ways to construct a filter that returns hotels that provide parking, for example: +For more ways to construct a filter, see [Constructing a filter](#constructing-a-filter). + +### Faiss k-NN filter implementation + +Starting with k-NN plugin version 2.9, you can use `faiss` filters for k-NN searches. + +When you specify a Faiss filter for a k-NN search, the Faiss algorithm decides whether to perform an exact k-NN search with pre-filtering or an approximate search with modified post-filtering. The algorithm uses the following variables: + +- N: The number of documents in the index. +- P: The number of documents in the document subset after the filter is applied (P <= N). +- k: The maximum number of vectors to return in the response. + +The following flow chart outlines the Faiss algorithm. + +![Faiss algorithm for filtering]({{site.url}}{{site.baseurl}}/images/faiss-algorithm.jpg) + +### Using a Faiss efficient filter + +Consider an index that contains information about different shirts for an e-commerce application. You want to find the top-rated shirts that are similar to the one you already have but would like to restrict the results by shirt size. + +In this example, you will create an index and search for shirts that are similar to the shirt you provide. + +**Step 1: Create a new index** + +Before you can run a k-NN search with a filter, you need to create an index with a `knn_vector` field. For this field, you need to specify `faiss` and `hnsw` as the `method` in the mapping. + +The following request creates an index that contains vector representations of shirts: + +```json +PUT /products-shirts +{ + "settings": { + "index": { + "knn": true + } + }, + "mappings": { + "properties": { + "item_vector": { + "type": "knn_vector", + "dimension": 3, + "method": { + "name": "hnsw", + "space_type": "l2", + "engine": "faiss" + } + } + } + } +} +``` +{% include copy-curl.html %} + +**Step 2: Add data to your index** + +Next, add data to your index. + +The following request adds 12 documents that contain information about shirts, including their vector representation, size, and rating: + +```json +POST /_bulk?refresh +{ "index": { "_index": "products-shirts", "_id": "1" } } +{ "item_vector": [5.2, 4.4, 8.4], "size" : "large", "rating" : 5 } +{ "index": { "_index": "products-shirts", "_id": "2" } } +{ "item_vector": [5.2, 3.9, 2.9], "size" : "small", "rating" : 4 } +{ "index": { "_index": "products-shirts", "_id": "3" } } +{ "item_vector": [4.9, 3.4, 2.2], "size" : "xlarge", "rating" : 9 } +{ "index": { "_index": "products-shirts", "_id": "4" } } +{ "item_vector": [4.2, 4.6, 5.5], "size" : "large", "rating" : 6} +{ "index": { "_index": "products-shirts", "_id": "5" } } +{ "item_vector": [3.3, 4.5, 8.8], "size" : "medium", "rating" : 8 } +{ "index": { "_index": "products-shirts", "_id": "6" } } +{ "item_vector": [6.4, 3.4, 6.6], "size" : "small", "rating" : 9 } +{ "index": { "_index": "products-shirts", "_id": "7" } } +{ "item_vector": [4.2, 6.2, 4.6], "size" : "small", "rating" : 5 } +{ "index": { "_index": "products-shirts", "_id": "8" } } +{ "item_vector": [2.4, 4.0, 3.0], "size" : "small", "rating" : 8 } +{ "index": { "_index": "products-shirts", "_id": "9" } } +{ "item_vector": [1.4, 3.2, 9.0], "size" : "small", "rating" : 5 } +{ "index": { "_index": "products-shirts", "_id": "10" } } +{ "item_vector": [7.0, 9.9, 9.0], "size" : "xlarge", "rating" : 9 } +{ "index": { "_index": "products-shirts", "_id": "11" } } +{ "item_vector": [3.0, 2.3, 2.0], "size" : "large", "rating" : 6 } +{ "index": { "_index": "products-shirts", "_id": "12" } } +{ "item_vector": [5.0, 1.0, 4.0], "size" : "large", "rating" : 3 } + +``` +{% include copy-curl.html %} + +**Step 3: Search your data with a filter** + +Now you can create a k-NN search with filters. In the k-NN query clause, include the vector representation of the shirt that is used to search for similar ones, the number of nearest neighbors to return (`k`), and a filter by size and rating. + +The following request searches for size small shirts rated between 7 and 10, inclusive: + +```json +POST /products-shirts/_search +{ + "size": 2, + "query": { + "knn": { + "item_vector": { + "vector": [ + 2, 4, 3 + ], + "k": 10, + "filter": { + "bool": { + "must": [ + { + "range": { + "rating": { + "gte": 7, + "lte": 10 + } + } + }, + { + "term": { + "size": "small" + } + } + ] + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +The response returns the two matching documents: + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": 0.8620689, + "hits": [ + { + "_index": "products-shirts", + "_id": "8", + "_score": 0.8620689, + "_source": { + "item_vector": [ + 2.4, + 4, + 3 + ], + "size": "small", + "rating": 8 + } + }, + { + "_index": "products-shirts", + "_id": "6", + "_score": 0.029691212, + "_source": { + "item_vector": [ + 6.4, + 3.4, + 6.6 + ], + "size": "small", + "rating": 9 + } + } + ] + } +} +``` + +For more ways to construct a filter, see [Constructing a filter](#constructing-a-filter). + +### Constructing a filter + +There are multiple ways to construct a filter for the same condition. For example, you can use the following constructs to create a filter that returns hotels that provide parking: - A `term` query clause in the `should` clause - A `wildcard` query clause in the `should` clause @@ -466,4 +507,196 @@ POST /hotels-index/_search } } ``` +{% include copy-curl.html %} + +## Post-filtering + +You can achieve post-filtering with a Boolean filter or by providing the `post_filter` parameter. + +### Boolean filter with ANN search + +A Boolean filter consists of a Boolean query that contains a k-NN query and a filter. For example, the following query searches for hotels that are closest to the specified `location` and then filters the results to return hotels with a rating between 8 and 10, inclusive, that provide parking: + +```json +POST /hotels-index/_search +{ + "size": 3, + "query": { + "bool": { + "filter": { + "bool": { + "must": [ + { + "range": { + "rating": { + "gte": 8, + "lte": 10 + } + } + }, + { + "term": { + "parking": "true" + } + } + ] + } + }, + "must": [ + { + "knn": { + "location": { + "vector": [ + 5, + 4 + ], + "k": 20 + } + } + } + ] + } + } +} +``` + +The response includes documents containing the matching hotels: + +```json +{ + "took" : 95, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 5, + "relation" : "eq" + }, + "max_score" : 0.72992706, + "hits" : [ + { + "_index" : "hotels-index", + "_id" : "3", + "_score" : 0.72992706, + "_source" : { + "location" : [ + 4.9, + 3.4 + ], + "parking" : "true", + "rating" : 9 + } + }, + { + "_index" : "hotels-index", + "_id" : "6", + "_score" : 0.3012048, + "_source" : { + "location" : [ + 6.4, + 3.4 + ], + "parking" : "true", + "rating" : 9 + } + }, + { + "_index" : "hotels-index", + "_id" : "5", + "_score" : 0.24154587, + "_source" : { + "location" : [ + 3.3, + 4.5 + ], + "parking" : "true", + "rating" : 8 + } + } + ] + } +} +``` + +### post-filter parameter + +If you use the `knn` query alongside filters or other clauses (for example, `bool`, `must`, `match`), you might receive fewer than `k` results. In this example, `post_filter` reduces the number of results from 2 to 1: + +```json +GET my-knn-index-1/_search +{ + "size": 2, + "query": { + "knn": { + "my_vector2": { + "vector": [2, 3, 5, 6], + "k": 2 + } + } + }, + "post_filter": { + "range": { + "price": { + "gte": 5, + "lte": 10 + } + } + } +} +``` + +## Scoring script filter + +A scoring script filter first filters the documents and then uses a brute-force exact k-NN search on the results. For example, the following query searches for hotels with a rating between 8 and 10, inclusive, that provide parking and then performs a k-NN search to return the 3 hotels that are closest to the specified `location`: + +```json +POST /hotels-index/_search +{ + "size": 3, + "query": { + "script_score": { + "query": { + "bool": { + "filter": { + "bool": { + "must": [ + { + "range": { + "rating": { + "gte": 8, + "lte": 10 + } + } + }, + { + "term": { + "parking": "true" + } + } + ] + } + } + } + }, + "script": { + "source": "knn_score", + "lang": "knn", + "params": { + "field": "location", + "query_value": [ + 5.0, + 4.0 + ], + "space_type": "l2" + } + } + } + } +} +``` {% include copy-curl.html %} \ No newline at end of file diff --git a/images/faiss-algorithm.jpg b/images/faiss-algorithm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e992c83f145b314bb7451c95efaeec92634bdda0 GIT binary patch literal 26341 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