From 6c83dfd87cc98949a36a20a32f3bc088b6ff7e78 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 | 807 +++++++++++++++-------- images/faiss-algorithm.jpg | Bin 0 -> 26341 bytes 3 files changed, 521 insertions(+), 310 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 d57d1640..13505b04 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 2e886ad5..7e926a97 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,56 +43,71 @@ 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 | +| 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 | -## Scoring script filter +## Efficient k-NN filtering -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`: +You can perform efficient k-NN filtering with the `lucene` or `faiss` engines. + +### 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. + +When you specify a Lucene filter for a k-NN search, the Lucene 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 Lucene algorithm. + +![Lucene algorithm for filtering]({{site.url}}{{site.baseurl}}/images/lucene-algorithm.png) + +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 + +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: + +![Graph of documents with filter criteria]({{site.url}}{{site.baseurl}}/images/knn-doc-set-for-filtering.png) + +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** + +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. + +The following request creates a new index called `hotels-index` with a `knn-filter` field called `location`: ```json -POST /hotels-index/_search +PUT /hotels-index { - "size": 3, - "query": { - "script_score": { - "query": { - "bool": { - "filter": { - "bool": { - "must": [ - { - "range": { - "rating": { - "gte": 8, - "lte": 10 - } - } - }, - { - "term": { - "parking": "true" - } - } - ] - } + "settings": { + "index": { + "knn": true, + "knn.algo_param.ef_search": 100, + "number_of_shards": 1, + "number_of_replicas": 0 + } + }, + "mappings": { + "properties": { + "location": { + "type": "knn_vector", + "dimension": 2, + "method": { + "name": "hnsw", + "space_type": "l2", + "engine": "lucene", + "parameters": { + "ef_construction": 100, + "m": 16 } } - }, - "script": { - "source": "knn_score", - "lang": "knn", - "params": { - "field": "location", - "query_value": [ - 5.0, - 4.0 - ], - "space_type": "l2" - } } } } @@ -88,7 +115,405 @@ POST /hotels-index/_search ``` {% include copy-curl.html %} -## Boolean filter with ANN search +**Step 2: Add data to your index** + +Next, add data to your index. + +The following request adds 12 documents that contain hotel location, rating, and parking information: + +```json +POST /_bulk +{ "index": { "_index": "hotels-index", "_id": "1" } } +{ "location": [5.2, 4.4], "parking" : "true", "rating" : 5 } +{ "index": { "_index": "hotels-index", "_id": "2" } } +{ "location": [5.2, 3.9], "parking" : "false", "rating" : 4 } +{ "index": { "_index": "hotels-index", "_id": "3" } } +{ "location": [4.9, 3.4], "parking" : "true", "rating" : 9 } +{ "index": { "_index": "hotels-index", "_id": "4" } } +{ "location": [4.2, 4.6], "parking" : "false", "rating" : 6} +{ "index": { "_index": "hotels-index", "_id": "5" } } +{ "location": [3.3, 4.5], "parking" : "true", "rating" : 8 } +{ "index": { "_index": "hotels-index", "_id": "6" } } +{ "location": [6.4, 3.4], "parking" : "true", "rating" : 9 } +{ "index": { "_index": "hotels-index", "_id": "7" } } +{ "location": [4.2, 6.2], "parking" : "true", "rating" : 5 } +{ "index": { "_index": "hotels-index", "_id": "8" } } +{ "location": [2.4, 4.0], "parking" : "true", "rating" : 8 } +{ "index": { "_index": "hotels-index", "_id": "9" } } +{ "location": [1.4, 3.2], "parking" : "false", "rating" : 5 } +{ "index": { "_index": "hotels-index", "_id": "10" } } +{ "location": [7.0, 9.9], "parking" : "true", "rating" : 9 } +{ "index": { "_index": "hotels-index", "_id": "11" } } +{ "location": [3.0, 2.3], "parking" : "false", "rating" : 6 } +{ "index": { "_index": "hotels-index", "_id": "12" } } +{ "location": [5.0, 1.0], "parking" : "true", "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 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. + +The following request creates a k-NN query that searches for the top three hotels near the location with the coordinates `[5, 4]` that are rated between 8 and 10, inclusive, and provide parking: + +```json +POST /hotels-index/_search +{ + "size": 3, + "query": { + "knn": { + "location": { + "vector": [ + 5, + 4 + ], + "k": 3, + "filter": { + "bool": { + "must": [ + { + "range": { + "rating": { + "gte": 8, + "lte": 10 + } + } + }, + { + "term": { + "parking": "true" + } + } + ] + } + } + } + } + } +} +``` +{% include copy-curl.html %} + +The response returns the three hotels that are nearest to the search point and have met the filter criteria: + +```json +{ + "took" : 47, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 3, + "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 + } + } + ] + } +} +``` + +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 +- A `regexp` query clause in the `should` clause +- A `must_not` clause to eliminate hotels with `parking` set to `false`. + +The following request illustrates these four different ways of searching for hotels with parking: + +```json +POST /hotels-index/_search +{ + "size": 3, + "query": { + "knn": { + "location": { + "vector": [ 5.0, 4.0 ], + "k": 3, + "filter": { + "bool": { + "must": { + "range": { + "rating": { + "gte": 1, + "lte": 6 + } + } + }, + "should": [ + { + "term": { + "parking": "true" + } + }, + { + "wildcard": { + "parking": { + "value": "t*e" + } + } + }, + { + "regexp": { + "parking": "[a-zA-Z]rue" + } + } + ], + "must_not": [ + { + "term": { + "parking": "false" + } + } + ], + "minimum_should_match": 1 + } + } + } + } + } +} +``` +{% 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: @@ -198,272 +623,80 @@ The response includes documents containing the matching hotels: } ``` -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). +### post-filter parameter -## 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. - -When you specify a Lucene filter for a k-NN search, the Lucene 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 Lucene algorithm. - -![Lucene algorithm for filtering]({{site.url}}{{site.baseurl}}/images/lucene-algorithm.png) - -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 - -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: - -![Graph of documents with filter criteria]({{site.url}}{{site.baseurl}}/images/knn-doc-set-for-filtering.png) - -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 - -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. - -The following request creates a new index called `hotels-index` with a `knn-filter` field called `location`: +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 -PUT /hotels-index +GET my-knn-index-1/_search { - "settings": { - "index": { - "knn": true, - "knn.algo_param.ef_search": 100, - "number_of_shards": 1, - "number_of_replicas": 0 + "size": 2, + "query": { + "knn": { + "my_vector2": { + "vector": [2, 3, 5, 6], + "k": 2 + } } }, - "mappings": { - "properties": { - "location": { - "type": "knn_vector", - "dimension": 2, - "method": { - "name": "hnsw", - "space_type": "l2", - "engine": "lucene", - "parameters": { - "ef_construction": 100, - "m": 16 - } - } + "post_filter": { + "range": { + "price": { + "gte": 5, + "lte": 10 } } } } ``` -{% include copy-curl.html %} -### Step 2: Add data to your index +## Scoring script filter -Next, add data to your index. - -The following request adds 12 documents that contain hotel location, rating, and parking information: - -```json -POST /_bulk -{ "index": { "_index": "hotels-index", "_id": "1" } } -{ "location": [5.2, 4.4], "parking" : "true", "rating" : 5 } -{ "index": { "_index": "hotels-index", "_id": "2" } } -{ "location": [5.2, 3.9], "parking" : "false", "rating" : 4 } -{ "index": { "_index": "hotels-index", "_id": "3" } } -{ "location": [4.9, 3.4], "parking" : "true", "rating" : 9 } -{ "index": { "_index": "hotels-index", "_id": "4" } } -{ "location": [4.2, 4.6], "parking" : "false", "rating" : 6} -{ "index": { "_index": "hotels-index", "_id": "5" } } -{ "location": [3.3, 4.5], "parking" : "true", "rating" : 8 } -{ "index": { "_index": "hotels-index", "_id": "6" } } -{ "location": [6.4, 3.4], "parking" : "true", "rating" : 9 } -{ "index": { "_index": "hotels-index", "_id": "7" } } -{ "location": [4.2, 6.2], "parking" : "true", "rating" : 5 } -{ "index": { "_index": "hotels-index", "_id": "8" } } -{ "location": [2.4, 4.0], "parking" : "true", "rating" : 8 } -{ "index": { "_index": "hotels-index", "_id": "9" } } -{ "location": [1.4, 3.2], "parking" : "false", "rating" : 5 } -{ "index": { "_index": "hotels-index", "_id": "10" } } -{ "location": [7.0, 9.9], "parking" : "true", "rating" : 9 } -{ "index": { "_index": "hotels-index", "_id": "11" } } -{ "location": [3.0, 2.3], "parking" : "false", "rating" : 6 } -{ "index": { "_index": "hotels-index", "_id": "12" } } -{ "location": [5.0, 1.0], "parking" : "true", "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 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. - -The following request creates a k-NN query that searches for the top three hotels near the location with the coordinates `[5, 4]` that are rated between 8 and 10, inclusive, and provide parking: +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": { - "knn": { - "location": { - "vector": [ - 5, - 4 - ], - "k": 3, - "filter": { - "bool": { - "must": [ - { - "range": { - "rating": { - "gte": 8, - "lte": 10 + "script_score": { + "query": { + "bool": { + "filter": { + "bool": { + "must": [ + { + "range": { + "rating": { + "gte": 8, + "lte": 10 + } + } + }, + { + "term": { + "parking": "true" } } - }, - { - "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 %} - -The response returns the three hotels that are nearest to the search point and have met the filter criteria: - -```json -{ - "took" : 47, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 - }, - "hits" : { - "total" : { - "value" : 3, - "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 - } - } - ] - } -} -``` - -Note that there are multiple ways to construct a filter that returns hotels that provide parking, for example: - -- A `term` query clause in the `should` clause -- A `wildcard` query clause in the `should` clause -- A `regexp` query clause in the `should` clause -- A `must_not` clause to eliminate hotels with `parking` set to `false`. - -The following request illustrates these four different ways of searching for hotels with parking: - -```json -POST /hotels-index/_search -{ - "size": 3, - "query": { - "knn": { - "location": { - "vector": [ 5.0, 4.0 ], - "k": 3, - "filter": { - "bool": { - "must": { - "range": { - "rating": { - "gte": 1, - "lte": 6 - } - } - }, - "should": [ - { - "term": { - "parking": "true" - } - }, - { - "wildcard": { - "parking": { - "value": "t*e" - } - } - }, - { - "regexp": { - "parking": "[a-zA-Z]rue" - } - } - ], - "must_not": [ - { - "term": { - "parking": "false" - } - } - ], - "minimum_should_match": 1 - } - } - } - } - } -} -``` {% 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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