diff --git a/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-csharp.mdx b/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-csharp.mdx index 4001ed9b44..7d5482b2cd 100644 --- a/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-csharp.mdx +++ b/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-csharp.mdx @@ -1016,9 +1016,10 @@ It also reduces computational overhead, making operations like similarity search #### Quantization in RavenDB: For non-quantized raw 32-bit data or text stored in your documents, -RavenDB allows you to choose the quantization format for the generated embeddings stored in the index. -The selected quantization type determines the similarity search technique that will be applied. +RavenDB allows you to choose the quantization format for the generated embeddings stored in the index. +The quantization format applies to how vectors are stored internally in the index, regardless of the client language used to submit the data. +The selected quantization type determines the similarity search technique that will be applied. If no target quantization format is specified, the `Single` option will be used as the default. The available quantization options are: @@ -1322,7 +1323,7 @@ where vector.search(TagsEmbeddedAsSingle, $queryVector, 0.85, 10) * You can perform a vector search and a regular search in the same query. A single auto-index will be created for both search predicates. -* In the following example, results will include Product documents with content similar to "Italian food" in their _Name_ field and a _PricePerUnit_ above 20. +* In the following example, results will include Product documents with content similar to "italian food" in their _Name_ field and a _PricePerUnit_ above 20. The following auto-index will be generated: `Auto/Products/ByPricePerUnitAndVector.search(embedding.text(Name))`. @@ -1610,7 +1611,7 @@ or `VectorSearch`: - + ```csharp public IRavenQueryable VectorSearch( Func, IVectorEmbeddingTextField> textFieldFactory, @@ -1648,14 +1649,14 @@ public IRavenQueryable VectorSearch( | **isExact** | `bool` | `false` - vector search will be performed in an approximate manner.
`true` - vector search will be performed in an exact manner. | The default value for `minimumSimilarity` is defined by this configuration key: -[Indexing.Corax.VectorSearch.DefaultMinimumSimilarity ](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultnumberofcandidatesforquerying). +[Indexing.Corax.VectorSearch.DefaultMinimumSimilarity ](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultminimumsimilarity). The default value for `numberOfCandidates` is defined by this configuration key: -[Indexing.Corax.VectorSearch.DefaultNumberOfCandidatesForQuerying](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultminimumsimilarity). +[Indexing.Corax.VectorSearch.DefaultNumberOfCandidatesForQuerying](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultnumberofcandidatesforquerying). `IVectorFieldFactory`: - + ```csharp public interface IVectorFieldFactory { @@ -1694,7 +1695,7 @@ public interface IVectorFieldFactory `IVectorEmbeddingTextField` & `IVectorEmbeddingField`: - + ```csharp public interface IVectorEmbeddingTextField { @@ -1718,7 +1719,7 @@ public interface IVectorEmbeddingField | **targetEmbeddingQuantization** | `VectorEmbeddingType` | The desired target quantization format. | | **embeddingsGenerationTaskIdentifier** | `string ` | The identifier of an embeddings generation task.
Used to locate the embeddings generated by the task in the [Embedding collections](../../../ai-integration/generating-embeddings/embedding-collections.mdx). | - + ```csharp public enum VectorEmbeddingType { @@ -1732,7 +1733,7 @@ public enum VectorEmbeddingType `IVectorEmbeddingTextFieldValueFactory` & `IVectorEmbeddingFieldValueFactory`: - + ```csharp public interface IVectorEmbeddingTextFieldValueFactory { @@ -1780,7 +1781,7 @@ public interface IVectorEmbeddingFieldValueFactory RavenVector is RavenDB's dedicated data type for storing and querying numerical embeddings. Learn more in [RavenVector](../../../ai-integration/vector-search/data-types-for-vector-search.mdx#ravenvector). - + ```csharp public class RavenVector() { @@ -1795,7 +1796,7 @@ RavenDB provides the following quantizer methods. Use them to transform your raw data to the desired format. Other quantizers may not be compatible. - + ```csharp public static class VectorQuantizer { diff --git a/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-nodejs.mdx b/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-nodejs.mdx new file mode 100644 index 0000000000..f2357f7b32 --- /dev/null +++ b/docs/ai-integration/vector-search/content/_vector-search-using-dynamic-query-nodejs.mdx @@ -0,0 +1,1276 @@ +import Admonition from '@theme/Admonition'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeBlock from '@theme/CodeBlock'; + + + +* This article explains how to run a vector search using a **dynamic query**. + To learn how to run a vector search using a static-index, see [vector search using a static-index](../../../ai-integration/vector-search/vector-search-using-static-index.mdx). + +* In this article: + * [What is a vector search](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#what-is-a-vector-search) + * [Dynamic vector search - query overview](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---query-overview) + * [Creating embeddings for the auto-index](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#creating-embeddings-for-the-auto-index) + * [Retrieving results](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#retrieving-results) + * [The dynamic query parameters](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#the-dynamic-query-parameters) + * [Corax auto-indexes](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#corax-auto-indexes) + * [Dynamic vector search - querying TEXT](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-text) + * [Querying raw text](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#querying-raw-text) + * [Querying pre-made embeddings generated by tasks](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#querying-pre-made-embeddings-generated-by-tasks) + * [Dynamic vector search - querying NUMERICAL content](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-numerical-content) + * [Dynamic vector search - querying for similar documents](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-for-similar-documents) + * [Dynamic vector search - exact search](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---exact-search) + * [Quantization options](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#quantization-options) + * [Querying vector fields and regular data in the same query](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#querying-vector-fields-and-regular-data-in-the-same-query) + * [Combining multiple vector searches in the same query](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#combining-multiple-vector-searches-in-the-same-query) + * [Syntax](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#syntax) + + + +## What is a vector search + +* Vector search is a method for finding documents based on their **contextual similarity** to the search item provided in a given query. + +* Your data is converted into vectors, known as **embeddings**, and stored in a multidimensional space. + Unlike traditional keyword-based searches, which rely on exact matches, + vector search identifies vectors closest to your query vector and retrieves the corresponding documents. + +## Dynamic vector search - query overview + + + +#### Overview + +* A dynamic vector search query can be performed on: + * Raw text stored in your documents. + * Pre-made embeddings that you created yourself and stored using these [Data types](../../../ai-integration/vector-search/data-types-for-vector-search.mdx). + * Pre-made embeddings that are automatically generated from your document content + by RavenDB's [Embeddings generation tasks](../../../ai-integration/generating-embeddings/overview.mdx) using external service providers. + +* Note: Vector search queries cannot be used with [Subscription queries](../../../client-api/data-subscriptions/creation/api-overview.mdx#subscription-query). + +* When executing a dynamic vector search query, RavenDB creates a [Corax Auto-Index](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#corax-auto-indexes) to process the query, + and the results are retrieved from that index. + +* To make a **dynamic vector search query**: + * From the Client API - use method `vectorSearch()` + * In RQL - use method `vector.search()` + * Examples are provided below + + + + + +#### Creating embeddings for the Auto-index + +* **Creating embeddings from TEXTUAL content**: + + * **Pre-made embeddings via tasks**: + Embeddings can be created from textual content in your documents by defining [Tasks that generate embeddings](../../../ai-integration/generating-embeddings/overview.mdx). + When performing a dynamic vector search query over textual data and explicitly specifying the task, + results will be retrieved by comparing your search term against the embeddings previously generated by that task. + A query example is available in: [Querying pre-made embeddings generated by tasks](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#querying-pre-made-embeddings-generated-by-tasks). + + * **Default embeddings generation**: + When querying textual data without specifying a task, RavenDB generates an embedding vector for the specified document field in each document of the queried collection, + using the built-in [bge-micro-v2](https://huggingface.co/TaylorAI/bge-micro-v2) sentence-transformer model. + A query example is available in: [Querying raw text](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#querying-raw-text). + +* **Creating embeddings from NUMERICAL arrays**: + When querying over pre-made numerical arrays that are already in vector format, + RavenDB will index them without transformation (unless further quantization is applied). + A query example is available in: [Vector search on numerical content](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-numerical-content). + + To avoid index errors, ensure that the dimensionality of these numerical arrays (i.e., their length) + is consistent across all your source documents for the field you are querying. + If you wish to enforce such consistency - + perform a vector search using a [Static-index](../../../ai-integration/vector-search/vector-search-using-static-index.mdx) instead of a dynamic query. + + +* **Quantizing the embeddings**: + The embeddings are quantized based on the parameters specified in the query. + Learn more about quantization in [Quantization options](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#quantization-options). + +* **Indexing the embeddings**: + RavenDB indexes the embeddings on the server using the [HNSW algorithm](https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world). + This algorithm organizes embeddings into a high-dimensional graph structure, + enabling efficient retrieval of Approximate Nearest Neighbors (ANN) during queries. + + + + + +#### Retrieving results + +* **Processing the query**: + To ensure consistent comparisons, the **search term** is transformed into an embedding vector using the same method as the document fields. + The server will search for the most similar vectors in the indexed vector space, + taking into account all the [query parameters](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#the-dynamic-query-parameters) described below. + The documents that correspond to the resulting vectors are then returned to the client. + +* **Search results**: + By default, the resulting documents will be ordered by their score. + You can modify this behavior using the [Indexing.Corax.VectorSearch.OrderByScoreAutomatically](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchorderbyscoreautomatically) configuration key. + In addition, you can apply any of the 'order by' methods to your query, as explained in [sort query results](../../../client-api/session/querying/sort-query-results.mdx). + + + + + +#### The dynamic query parameters + +* **Source data format** + RavenDB supports performing vector search on TEXTUAL values or NUMERICAL arrays. + the source data can be formatted as `Text`, `Single`, `Int8`, or `Binary`. + +* **Target quantization** + You can specify the quantization encoding for the embeddings that will be created from source data. + Learn more about quantization in [Quantization options](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#quantization-options). + +* **Minimum similarity** + You can specify the minimum similarity to use when searching for related vectors. + The value can be between `0.0` and `1.0`. + * A value closer to `1.0` requires higher similarity between vectors, + while a value closer to `0.0` allows for less similarity. + * **Important**: To filter out less relevant results when performing vector search queries, + it is recommended to explicitly specify the minimum similarity level at query time. + + If not specified, the default value is taken from the following configuration key: + [Indexing.Corax.VectorSearch.DefaultMinimumSimilarity](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultminimumsimilarity). + +* **Number of candidates** + You can specify the maximum number of vectors that RavenDB will return from a graph search. + The number of the resulting documents that correspond to these vectors may be: + * lower than the number of candidates - when multiple vectors originated from the same document. + * higher than the number of candidates - when the same vector is shared between multiple documents. + + If not specified, the default value is taken from the following configuration key: + [Indexing.Corax.VectorSearch.DefaultNumberOfCandidatesForQuerying](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultnumberofcandidatesforquerying). + +* **Search method** + * _Approximate Nearest-Neighbor search_ (Default): + Search for related vectors in an approximate manner, providing faster results. + * _Exact search_: + Perform a thorough scan of the vectors to find the actual closest vectors, + offering better accuracy but at a higher computational cost. + Learn more in [Exact search](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---exact-search). + + + + + +#### Corax auto-indexes + +* Only [Corax indexes](../../../indexes/search-engine/corax.mdx) support vector search. + +* Even if your **default auto-index engine** is set to Lucene (via [Indexing.Auto.SearchEngineType](../../../server/configuration/indexing-configuration.mdx#indexingautosearchenginetype)), + performing a vector search using a dynamic query will create a new auto-index based on Corax. + +* Normally, new dynamic queries extend existing [auto-indexes](../../../client-api/session/querying/how-to-query.mdx#queries-always-provide-results-using-an-index) if they require additional fields. + However, a dynamic query with a vector search will not extend an existing Lucene-based auto-index. + + + For example, suppose you have an existing **Lucene**-based auto-index on the Employees collection: e.g.: + `Auto/Employees/ByFirstName`. + + Now, you run a query that: + + * searches for Employees by _LastName_ (a regular text search) + * and performs a vector search over the _Notes_ field. + + The following new **Corax**-based auto-index will be created: + `Auto/Employees/ByLastNameAndVector.search(embedding.text(Notes))`, + and the existing **Lucene** index on Employees will not be deleted or extended. + + + + +## Dynamic vector search - querying TEXT + +### Querying raw text + +* The following example searches for Product documents where the _'Name'_ field is similar to the search term `"italian food"`. + +* Since the query does Not specify an [Embeddings generation task](../../../ai-integration/generating-embeddings/overview.mdx), + RavenDB dynamically generates embedding vectors for the _'Name'_ field of each document in the queried collection using the built-in + [bge-micro-v2](https://huggingface.co/TaylorAI/bge-micro-v2) text-embedding model. + The generated embeddings are indexed within the auto-index. + Unlike embeddings pre-made by tasks, this process does not create dedicated collections for storing embeddings. + +* Since this query does not specify a target quantization format, + the generated embedding vectors will be encoded in the default _Single_ format (single-precision floating-point). + Refer to [Quantization options](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#quantization-options) for examples that specify the destination quantization. + + + +```js +const similarProducts = await session.query({collection: "Products"}) + // Perform a vector search + // Call the 'vectorSearch' method + .vectorSearch( + // Call 'withText' + // Specify the document field in which to search for similar values + field => field.withText("Name"), + // Call 'byText' + // Provide the term for the similarity comparison + searchTerm => searchTerm.byText("italian food"), { + // It is recommended to specify the minimum similarity level + similarity: 0.82, + // Optionally, specify the number of candidates for the search + numberOfCandidates: 20 + }) + // Waiting for not-stale results is not mandatory + // but will assure results are not stale + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarProducts = await session.advanced + .rawQuery(` + from 'Products' + // Wrap the document field 'Name' with 'embedding.text' to indicate the source data type + where vector.search(embedding.text(Name), $searchTerm, 0.82, 20)`) + .addParameter("searchTerm", "italian food") + .waitForNonStaleResults() + .all(); +``` + + +```sql +// Query the Products collection +from "Products" +// Call 'vector.search' +// Wrap the document field 'Name' with 'embedding.text' to indicate the source data type +where vector.search(embedding.text(Name), "italian food", 0.82, 20) +``` + + + +* Executing the above query on the RavenDB sample data will create the following **auto-index**: + `Auto/Products/ByVector.search(embedding.text(Name))` + + ![Search for italian food 1](../assets/vector-search-1.png) + +* Running the same query at a lower similarity level will return more results related to _"Italian food"_ but they may be less similar: + + ![Search for italian food 2](../assets/vector-search-2.png) + +### Querying pre-made embeddings generated by tasks + +* The following example searches for Category documents where the _'Name'_ field is similar to the search term `"candy"`. + +* The query explicitly specifies the **identifier** of the embeddings generation task that was defined in + [this example](../../../ai-integration/generating-embeddings/embeddings-generation-task.mdx#configuring-an-embeddings-generation-task---from-the-studio). + An `InvalidQueryException` will be thrown if no embeddings generation task with the specified identifier exists. + + To avoid this error, you can verify that the specified embeddings generation task exists before issuing the query. + Refer to [Get embeddings generation task details](../ai-integration/generating-embeddings/overview#get-embeddings-generation-task-details) + to learn how to programmatically check which tasks are defined + and what their identifiers are. + +* Results are retrieved by comparing the search term against the pre-made embeddings generated by the specified task, + which are stored in the [Embedding collections](../../../ai-integration/generating-embeddings/embedding-collections.mdx). + To ensure consistent comparisons, the search term is transformed into an embedding using the same embeddings generation task. + + + +```js +const similarCategories = await session.query({collection: "Categories"}) + .vectorSearch( + field => field + // Call 'withText' + // Specify the document field in which to search for similar values + .withText("Name") + // Call 'usingTask' + // Specify the identifier of the task that generated + // the embeddings for the Name field + .usingTask("id-for-task-open-ai"), + // Call 'byText' + // Provide the search term for the similarity comparison + searchTerm => searchTerm.byText("candy"), + { + // It is recommended to specify the minimum similarity level + similarity: 0.75 + }) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarCategories = await session.advanced + .rawQuery(` + from 'Categories' + // Specify the identifier of the task that generated the embeddings inside 'ai.task' + where vector.search(embedding.text(Name, ai.task('id-for-task-open-ai')), $searchTerm, 0.75) + `) + .addParameter( "searchTerm", "candy") + .waitForNonStaleResults() + .all(); +``` + + +```sql +// Query the Categories collection +from "Categories" +// Call 'vector.search' +// Specify the identifier of the task that generated the embeddings inside the 'ai.task' method +where vector.search(embedding.text(Name, ai.task('id-for-task-open-ai')), $searchTerm, 0.75) +{"searchTerm": "candy"} +``` + + + +* Executing the above query on the RavenDB sample data will create the following **auto-index**: + `Auto/Categories/ByVector.search(embedding.text(Name|ai.task('id-for-task-open-ai')))` + +## Dynamic vector search - querying NUMERICAL content + +* The following examples will use the sample data shown below. + The _Movie_ class includes various formats of numerical vector data. + Note: This sample data is minimal to keep the examples simple. + +* Note the usage of RavenDB's dedicated data type, [RavenVector](../../../ai-integration/vector-search/data-types-for-vector-search.mdx#ravenvector), which is highly optimized for reading and writing arrays to disk. + Learn more about the source data types suitable for vector search in [Data types for vector search](../../../ai-integration/vector-search/data-types-for-vector-search.mdx). + +* Unlike vector searches on text, where RavenDB transforms the raw text into an embedding vector, + numerical vector searches require your source data to already be in an embedding vector format. + +* If your raw data is in a _float_ format, you can request further quantization of the embeddings that will be indexed in the auto-index. + See an example of this in: [Quantization options](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#quantization-options). + +* Raw data that is already formatted as _Int8_ or _Binary_ **cannot** be quantized to lower-form (e.g. Int8 -> Int1). + When storing data in these formats in your documents, you should use [RavenDB’s `vectorQuantizer` methods](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#vectorquanitzer). + +#### Sample data: + + + +```js +// Sample class representing a document with various formats of numerical vectors +// The embedding vectors for these fields here are generated externally by you (not by RavenDB). +class Movie { + id; + title; + + // This field will hold numerical vector data - Not quantized + tagsEmbeddedAsSingle; + + // This field will hold numerical vector data - Quantized to Int8 + tagsEmbeddedAsInt8; + + // This field will hold numerical vector data - Encoded in Base64 format + tagsEmbeddedAsBase64; + + // A field for holding a numerical vector data produced by a multimodal model + // that converts an image into an embedding + moviePhotoEmbedding; +} +``` + + +```js +const session = store.openSession(); + +const movie1 = new Movie(); +movie1.title = "Hidden Figures"; +movie1.id = "movies/1"; + +// Embedded vector represented as numeric values +movie1.tagsEmbeddedAsSingle = RavenVector([6.599999904632568, 7.699999809265137]); + +// Embedded vectors encoded in Base64 format +movie1.tagsEmbeddedAsBase64 = ["zczMPc3MTD6amZk+", "mpmZPs3MzD4AAAA/"]; + +// Array of embedded vectors quantized to Int8 +movie1.tagsEmbeddedAsInt8 = [VectorQuantizer.toInt8([0.1, 0.2]), VectorQuantizer.toInt8([0.3, 0.4])]; + +// Example of an image embedding +// In a real scenario, this vector would come from a multimodal model +// such as CLIP, OpenCLIP, or similar +movie1.moviePhotoEmbedding = RavenVector([0.123, -0.045, 0.987, 0.564, -0.321, 0.220]) + +const movie2 = new Movie(); +movie2.title = "The Shawshank Redemption"; +movie2.id = "movies/2"; + +movie2.tagsEmbeddedAsSingle = RavenVector([8.800000190734863, 9.899999618530273]); +movie2.tagsEmbeddedAsBase64 = ["zcxMPs3MTD9mZmY/", "zcxMPpqZmT4zMzM/"]; +movie2.tagsEmbeddedAsInt8 = [VectorQuantizer.toInt8([0.5, 0.6]), VectorQuantizer.toInt8([0.7, 0.8])]; +movie2.moviePhotoEmbedding = RavenVector([0.456, -0.056, 0.123, 0.899, -0.765, 0.881]); + +await session.store(movie1); +await session.store(movie2); +await session.saveChanges(); +``` + + +```js +{ + "title": "Hidden Figures", + + "tagsEmbeddedAsSingle": { + "@vector": [ + 6.599999904632568, + 7.699999809265137 + ] + }, + + "tagsEmbeddedAsInt8": [ + [ + 64, + 127, + -51, + -52, + 76, + 62 + ], + [ + 95, + 127, + -51, + -52, + -52, + 62 + ] + ], + + "tagsEmbeddedAsBase64": [ + "zczMPc3MTD6amZk+", + "mpmZPs3MzD4AAAA/" + ], + + "moviePhotoEmbedding": { + "@vector": [0.123, -0.045, 0.987, 0.564, -0.321, 0.220] + } + + "@metadata": { + "@collection": "Movies" + } +} +``` + + + +#### Examples: + +These examples search for Movie documents with vectors similar to the one provided in the query. + + + +* Search on the `tagsEmbeddedAsSingle` field, which contains numerical data. + + + +```js +const similarMovies = await session.query({collection: "Movies"}) + // Perform a vector search + // Call the 'vectorSearch' method + .vectorSearch( + // Call 'withEmbedding', specify: + // * The source field that contains the embedding in the document + // * The source embedding type + field => field.withEmbedding("tagsEmbeddedAsSingle", "Single"), + // Call 'byEmbedding' + // Provide the vector for the similarity comparison + queryVector => queryVector.byEmbedding([6.599999904632568, 7.699999809265137]), { + // It is recommended to specify the minimum similarity level + similarity: 0.85, + // Optionally, specify the number of candidates for the search + numberOfCandidates: 10 + }) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarMovies = await session.advanced + .rawQuery(` + from 'Movies' where vector.search(tagsEmbeddedAsSingle, $queryVector, 0.85, 10) + `) + .addParameter("queryVector", [6.599999904632568, 7.699999809265137]) + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Movies" +// The source document field type is interpreted as 'Single' by default +where vector.search(tagsEmbeddedAsSingle, $queryVector, 0.85, 10) +{ "queryVector" : { "@vector" : [6.599999904632568, 7.699999809265137] }} +``` + + + + + + +* Search on the `tagsEmbeddedAsInt8` field, + which contains numerical data that is already quantized in **_Int8_ format**. + + + +```js +const similarMovies = await session.query({collection: "Movies"}) + .vectorSearch( + // Call 'withEmbedding', specify: + // * The source field that contains the embeddings in the document + // * The source embedding type + field => field.withEmbedding("tagsEmbeddedAsInt8", "Int8"), + // Call 'byEmbedding' + // Provide the vector for the similarity comparison + // (provide a single vector from the vector list in the tagsEmbeddedAsInt8 field) + queryVector => queryVector.byEmbedding( + // The provided vector MUST be in the same format as was stored in your document + // Call 'VectorQuantizer.toInt8' to transform the raw data to the Int8 format + VectorQuantizer.toInt8([0.1, 0.2]) + )) + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Movies" +// Wrap the source document field name with 'embedding.i8' to indicate the source data type +where vector.search(embedding.i8(tagsEmbeddedAsInt8), $queryVector) +{ "queryVector" : [64, 127, -51, -52, 76, 62] } +``` + + + + + + +* Search on the `tagsEmbeddedAsBase64` field, + which contains numerical data represented in **_Base64_ format**. + + + +```js +const similarMovies = await session.query({collection: "Movies"}) + .vectorSearch( + // Call 'withBase64', specify: + // * The source field that contains the embeddings in the document + // * The source embedding type + // (the type from which the Base64 string was constructed) + field => field.withBase64("tagsEmbeddedAsBase64", "Single"), + // Call 'byBase64' + // Provide the Base64 string that represents the vector to query against + queryVector => queryVector.byBase64("zczMPc3MTD6amZk+")) + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Movies" +// * Wrap the source document field name using 'embedding.' to specify +// the source data type from which the Base64 string was generated. +// * If the document field is Not wrapped, 'single' is assumed as the default source type. +where vector.search(tagsEmbeddedAsBase64, $queryVectorBase64) +{ "queryVectorBase64" : "zczMPc3MTD6amZk+" } +``` + + + + + +## Dynamic vector search - querying for similar documents + +* In the above examples, to find documents with similar content, the query was given an arbitrary input - + either a [raw textual search term](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-text) + or a [numerical query vector](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#dynamic-vector-search---querying-numerical-content). + +* RavenDB also allows you to search for documents whose content is similar to an **existing document**: + + * To do so, use the `forDocument` method and specify the existing document ID. See the example below. + + * When performing a dynamic vector query over a field, index-entries are generated in the auto-index, + one per document in the collection. Each index-entry contains the document ID and the embedding vector for the queried field. + + * RavenDB retrieves the embedding that was indexed for the queried field in the specified document and uses it as the query vector for the similarity comparison. + + * The results will include documents whose indexed embeddings are most similar to the one stored in the referenced document’s index-entry. + + + +```js +const similarProducts = await session.query({collection: "Products"}) + // Perform a vector search + // Call the 'VectorSearch' method + .vectorSearch( + // Call 'withText' + // Specify the document field in which to search for similar values + field => field.withText("Name"), + // Call 'forDocument' + // Provide the document ID for which you want to find similar documents. + // The embedding stored in the auto-index for the specified document + // will be used as the "query vector". + embedding => embedding.forDocument("Products/7-A"), + { + similarity: 0.82 + }) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarMovies = await session.advanced + .rawQuery(` + from 'Products' + // Pass a document ID to the 'forDoc' method to find similar documents + where vector.search(embedding.text(Name), embedding.forDoc($documentID), 0.82) + `) + .addParameter("$documentID", "Products/7-A") + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Products" +// Pass a document ID to the 'forDoc' method to find similar documents +where vector.search(embedding.text(Name), embedding.forDoc($documentID), 0.82) +{"documentID" : "Products/7-A"} +``` + + + +Running the above example on RavenDB’s sample data returns the following documents that have similar content in their _Name_ field: +(Note: the results include the referenced document itself, _Products/7-A_) + + +```js +// ID: products/7-A ... Name: "Uncle Bob's Organic Dried Pears" +// ID: products/51-A ... Name: "Manjimup Dried Apples" +// ID: products/6-A ... Name: "Grandma's Boysenberry Spread" +``` + + +The auto-index generated by running the above dynamic query is: +`Auto/Products/ByVector.search(embedding.text(Name))` + +You can **view the index-entries** of this auto-index in the Studio's query view: + +![Query the auto index](../assets/view-auto-index-entries-1.png) + +1. Go to the Query view in the Studio +2. Query the index +3. Open the settings dialog: + +![Open the settings dialog](../assets/view-auto-index-entries-2.png) + +![The index entries](../assets/view-auto-index-entries-3.png) + +## Dynamic vector search - exact search + +* When performing a dynamic vector search query, you can specify whether to perform an **exact search** to find the closest similar vectors in the vector space: + * A thorough scan will be performed to find the actual closest vectors. + * This ensures better accuracy but comes at a higher computational cost. + +* If exact is Not specified, the search defaults to the **Approximate Nearest-Neighbor** (ANN) method, + which finds related vectors in an approximate manner, offering faster results. + +* The following example demonstrates how to specify the exact method in the query. + Setting the param is similar for both text and numerical content searches. + + + +```js +const similarProducts = await session.query({collection: "Products"}) + .vectorSearch( + field => field.withText("Name"), + searchTerm => searchTerm.byText("italian food"), + // Optionally, set the 'isExact' param to true to perform an Exact search + { + isExact: true + }) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarProducts = await session.advanced + .rawQuery(`from 'Products' + // Wrap the query with the 'exact()' method + where exact(vector.search(embedding.text(Name), $searchTerm))`) + .addParameter("searchTerm", "italian food") + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Products" +// Wrap the vector.search query with the 'exact()' method +where exact(vector.search(embedding.text(Name), "italian food")) +``` + + + +## Quantization options + +#### What is quantization: + +Quantization is a technique that reduces the precision of numerical data. +It converts high-precision values, such as 32-bit floating-point numbers, into lower-precision formats like 8-bit integers or binary representations. + +The quantization process, applied to each dimension (or item) in the numerical array, +serves as a form of compression by reducing the number of bits used to represent each value in the vector. +For example, transitioning from 32-bit floats to 8-bit integers significantly reduces data size while preserving the vector's essential structure. + +Although it introduces some precision loss, quantization minimizes storage requirements and optimizes memory usage. +It also reduces computational overhead, making operations like similarity searches faster and more efficient. + +#### Quantization in RavenDB: + +For non-quantized raw 32-bit data or text stored in your documents, +RavenDB allows you to choose the quantization format for the generated embeddings stored in the index. +The quantization format applies to how vectors are stored internally in the index, regardless of the client language used to submit the data. + +The selected quantization type determines the similarity search technique that will be applied. +If no target quantization format is specified, the `Single` option will be used as the default. + +The available quantization options are: + + * `Single` (a 32-bit floating point value per dimension): + Provides precise vector representations. + The [Cosine similarity](https://en.wikipedia.org/wiki/Cosine_similarity) method will be used for searching and matching. + + * `Int8` (an 8-bit integer value per dimension): + Reduces storage requirements while maintaining good performance. + Saves up to 75% storage compared to 32-bit floating-point values. + The Cosine similarity method will be used for searching and matching. + + * `Binary` (1-bit per dimension): + Minimizes storage usage, suitable for use cases where binary representation suffices. + Saves approximately 96% storage compared to 32-bit floating-point values. + The [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) method will be used for searching and matching. + + + If your documents contain data that is already quantized, + it cannot be re-quantized to a lower precision format (e.g., Int8 cannot be converted to Binary). + + +#### Examples + + + +* In this example: + * The source data consists of text. + * The generated embeddings will use the _Int8_ format. + + + +```js +const similarProducts = await session.query({collection: "Products"}) + .vectorSearch( + // Specify the source text field for the embeddings + field => field.withText("Name") + // Set the quantization type for the generated embeddings + .targetQuantization("Int8"), + // Provide the search term for comparison + searchTerm => searchTerm.byText("italian food"), + ) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarProducts = await session.advanced + .rawQuery(` + from 'Products' + // Wrap the 'Name' field with 'embedding.text_i8' + where vector.search(embedding.text_i8(Name), $searchTerm) + `) + .addParameter("searchTerm", "italian food") + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Products" +// Wrap the 'Name' field with 'embedding.text_i8' +where vector.search(embedding.text_i8(Name), $searchTerm) +{ "searchTerm" : "italian food" } +``` + + + + + + +* In this example: + * The source data is an array of 32-bit floats. + * The generated embeddings will use the _Binary_ format. + + + +```js +const similarMovies = await session.query({collection: "Movies"}) + .vectorSearch( + // Specify the source field and its type + field => field.withEmbedding("tagsEmbeddedAsSingle", "Single") + // Set the quantization type for the generated embeddings + .targetQuantization("Binary"), + // Provide the vector to use for comparison + queryVector => queryVector.byEmbedding([6.599999904632568, 7.699999809265137]), + ) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarMovies = await session.advanced + .rawQuery(` + from 'Movies' + // Wrap the 'tagsEmbeddedAsSingle' field with 'embedding.f32_i1' + where vector.search(embedding.f32_i1(tagsEmbeddedAsSingle), $queryVector) + `) + .addParameter("queryVector", [6.599999904632568, 7.699999809265137]) + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Movies" +// Wrap the 'TagsEmbeddedAsSingle' field with 'embedding.f32_i1' +where vector.search(embedding.f32_i1(tagsEmbeddedAsSingle), $queryVector) +{ "queryVector" : { "@vector" : [6.599999904632568,7.699999809265137] }} +``` + + + + + +#### Field configuration methods in RQL: + +The following methods are available for performing a vector search via RQL: + + + +* `embedding.text`: + Generates embeddings from text as multi-dimensional vectors with 32-bit floating-point values, + without applying quantization. + +* `embedding.text_i8`: + Generates embeddings from text as multi-dimensional vectors with 8-bit integer values. + +* `embedding.text_i1`: + Generates embeddings from text as multi-dimensional vectors in a binary format. +* `embedding.f32_i8`: + Converts multi-dimensional vectors with 32-bit floating-point values into vectors with 8-bit integer values. + +* `embedding.f32_i1`: + Converts multi-dimensional vectors with 32-bit floating-point values into vectors in a binary format. +* `embedding.i8`: + Indicates that the source data is already quantized as Int8 (cannot be further quantized). + +* `embedding.i1`: + Indicates that the source data is already quantized as binary (cannot be further quantized). + + + +Wrap the field name using any of the relevant methods listed above, based on your requirements. +For example, the following RQL encodes **text to Int8**: + + + +```sql +from "Products" +// Wrap the document field with 'embedding.text_i8' +where vector.search(embedding.text_i8(Name), "italian food", 0.82, 20) +``` + + + +When the field name is Not wrapped in any method, +the underlying values are treated as numerical values in the form of **32-bit floating-point** (Single) precision. +For example, the following RQL will use the floating-point values as they are, without applying further quantization: + + + +```sql +from "Movies" +// No wrapping +where vector.search(TagsEmbeddedAsSingle, $queryVector, 0.85, 10) +{"queryVector" : { "@vector" : [6.599999904632568, 7.699999809265137] }} +``` + + + +## Querying vector fields and regular data in the same query + +* You can perform a vector search and a regular search in the same query. + A single auto-index will be created for both search predicates. + +* In the following example, results will include Product documents with content similar to "italian food" in their _Name_ field and a _PricePerUnit_ above 20. + The following auto-index will be generated: + `Auto/Products/ByPricePerUnitAndVector.search(embedding.text(Name))`. + + + +```js +const similarProducts = await session.query({ collection: "Products" }) + // Perform a filtering condition: + .whereGreaterThan("PricePerUnit", 35) + // Perform a vector search: + .vectorSearch( + field => field.withText("Name"), + searchTerm => searchTerm.byText("italian food"), + 0.75, + 16 + ) + .waitForNonStaleResults() + .all(); +``` + + +```js +const similarProducts = await session.advanced + .rawQuery(` + from 'Products' + where (PricePerUnit > $minPrice) + and (vector.search(embedding.text(Name), $searchTerm, 0.75, 16)) + `) + .addParameter("minPrice", 35.0) + .addParameter("searchTerm", "italian food") + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Products" +// The filtering condition: +where (PricePerUnit > $minPrice) +and (vector.search(embedding.text(Name), $searchTerm, 0.75, 16)) +{ "minPrice" : 35.0, "searchTerm" : "italian food" } +``` + + + + + +**Impact of _numberOfCandidates_ on query results**: + +* When combining a vector search with a filtering condition, the filter applies only to the documents retrieved within the `numberOfCandidates` param limit. + Increasing or decreasing _numberOfCandidates_ can affect the query results. + A larger _numberOfCandidates_ increases the pool of documents considered, + improving the chances of finding results that match both the vector search and the filter condition. + +* For example, in the above query, the vector search executes with: similarity `0.75` and numberOfCandidates `16`. + Running this query on RavenDB's sample data returns **2** documents. + +* However, if you increase _numberOfCandidates_, the query will retrieve more candidate documents before applying the filtering condition. + If you run the following query: + + + +```sql +from "Products" +where (PricePerUnit > $minPrice) +// Run vector search with similarity 0.75 and numberOfCandidates 25 +and (vector.search(embedding.text(Name), $searchTerm, 0.75, 25)) +{ "minPrice" : 35.0, "searchTerm" : "italian food" } +``` + + + + now the query returns **4** documents instead of **2**. + + + +## Combining multiple vector searches in the same query + +* You can combine multiple vector search statements in the same query using logical operators. + This is useful when you want to retrieve documents that match more than one vector-based criterion. + +* In the example below, the results will include companies that match one of two vector search conditions: + * Companies from European countries with a _Name_ similar to "snack" + * Or companies with a _Name_ similar to "dairy" + +* Running the query example on the RavenDB sample data will generate the following auto-index: + `Auto/Companies/ByVector.search(embedding.text(Address.Country))AndVector.search(embedding.text(Name))`. + This index includes two vector fields: _Address.Country_ and _Name_. + + + +```js +const companies = await session.query({ collection: "Companies" }) + + // Use OpenSubclause & CloseSubclause to differentiate between clauses: + // ==================================================================== + + .openSubclause() + .vectorSearch( // Search for companies that sell snacks or similar + field => field.withText("Name"), + searchTerm => searchTerm.byText("snack"), + { + similarity: 0.78, + } + ) + // Use 'AndAlso' for an AND operation + .andAlso() + .vectorSearch( // Search for companies located in Europe + field => field.withText("Address.Country"), + searchTerm => searchTerm.byText("europe"), + { + similarity: 0.82, + } + ) + .closeSubclause() + // Use 'OrElse' for an OR operation + .orElse() + .openSubclause() + .vectorSearch( // Search for companies that sell dairy products or similar + field => field.withText("Name"), + v => v.byText("dairy"), + { + similarity: 0.80 + } + ) + .closeSubclause() + .waitForNonStaleResults() + .all() +``` + + +```js +const companies = await session.advanced.rawQuery(` + from Companies + where + ( + vector.search(embedding.text(Name), $searchTerm1, 0.78) + and + vector.search(embedding.text(Address.Country), $searchTerm2, 0.82) + ) + or + ( + vector.search(embedding.text(Name), $searchTerm3, 0.80) + ) + `) + .addParameter("searchTerm1", "snack") + .addParameter("searchTerm2", "europe") + .addParameter("searchTerm3", "dairy") + .waitForNonStaleResults() + .all(); +``` + + +```sql +from "Companies" +where +( + vector.search(embedding.text(Name), $searchTerm1, 0.78) + and + vector.search(embedding.text(Address.Country), $searchTerm2, 0.82) +) +or +( + vector.search(embedding.text(Name), $searchTerm3, 0.80) +) +{"searchTerm1" : "snack", "searchTerm2" : "europe", "searchTerm3" : "dairy"} +``` + + + + + +**How multiple vector search clauses are evaluated**: + +* Each vector search clause is evaluated independently - the search algorithm runs separately for each vector field. + +* Each clause retrieves a limited number of candidates, determined by the _NumberOfCandidates_ parameter. + * You can explicitly set this value in the query clause, see [query parameters](../../../ai-integration/vector-search/vector-search-using-dynamic-query.mdx#the-dynamic-query-parameters). + * If not specified, it is taken from the [Indexing.Corax.VectorSearch.DefaultNumberOfCandidatesForQuerying](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultnumberofcandidatesforquerying) configuration key (default is 16). + +* **The final result set** is computed by applying the logical operators (and, or) between these independently retrieved sets. + +* To improve the chances of getting intersecting results, consider increasing the _NumberOfCandidates_ in each vector search clause. + This expands the pool of documents considered by each clause, raising the likelihood of finding matches that satisfy the combined logic. + + + +## Syntax + +`vectorSearch`: + + +```js +vectorSearch( + fieldName, + valueFactory, + options +) +``` + + +| Parameter | Type | Description | +|--------------------|-----------------------------------------|----------------------------------------------------------------| +| **fieldName** | `string \| (factory) => (vector field)` | Either the name/path of the field to search,
or a factory callback that describes the vector field. | +| **valueFactory** | `number[] \| string \| (factory) => void` | The queried value(s): can be a numeric embedding,
a base64 string, or a callback that specifies how to supply the query value. | +| **options** | `object` | Optional vector search options. | + + +```js +// Shape of the options object +{ + numberOfCandidates, + similarity, + isExact +} +``` + + +| Parameter | Type | Description | +|------------------------|----------|----------------------------------------------------------------| +| **numberOfCandidates** | `number` | Number of candidate nodes for the HNSW algorithm. | +| **similarity** | `number` | Minimum similarity threshold for results (higher means more similar). | +| **isExact** | `boolean`| `false` - approximate vector search
`true` - exact vector search. | + +The default value for `similarity` is defined by this configuration key: +[Indexing.Corax.VectorSearch.DefaultMinimumSimilarity ](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultminimumsimilarity). + +The default value for `numberOfCandidates` is defined by this configuration key: +[Indexing.Corax.VectorSearch.DefaultNumberOfCandidatesForQuerying](../../../server/configuration/indexing-configuration.mdx#indexingcoraxvectorsearchdefaultnumberofcandidatesforquerying). + +--- + +**Methods available on field factory**: + + +```js +// Methods for the dynamic query: +// ============================== + +factory.withText(documentFieldName); +factory.withEmbedding(documentFieldName, storedEmbeddingQuantization /* optional */); +factory.withBase64(documentFieldName, storedEmbeddingQuantization /* optional */); + +// Method for querying a static index: +// =================================== + +factory.withField(indexFieldName); +``` + + +| Parameter | Type | Description | +|---------------------------------|----------|--------------------------------------------------------------------------------------------| +| **documentFieldName** | `string` | The name/path of the document field containing
text / embedding / base64 encoded data. | +| **indexFieldName** | `string` | The name of the index-field that vector search will be performed on. | +| **storedEmbeddingQuantization** | `string` | Quantization format of the stored embeddings.
Default: `Single` | + +**Objects returned by `withText()` / `withEmbedding()`**: + + +```js +// Returned by: factory.withText(fieldName) +textField + .targetQuantization(targetEmbeddingQuantization) + .usingTask(embeddingsGenerationTaskIdentifier); + +// Returned by: factory.withEmbedding(fieldName, storedEmbeddingQuantization?) +// or: factory.withBase64(fieldName, storedEmbeddingQuantization?) +embeddingField + .targetQuantization(targetEmbeddingQuantization); +``` + + +| Parameter | Type | Description | +|----------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **targetEmbeddingQuantization** | `string` | The desired target quantization format. | +| **embeddingsGenerationTaskIdentifier** | `string` | The identifier of an embeddings generation task.
Used to locate the embeddings generated by the task in the [Embedding collections](../../../ai-integration/generating-embeddings/embedding-collections.mdx). | + + +```js +// Values for `storedEmbeddingQuantization` and `targetEmbeddingQuantization`: +"Single" // float32 embeddings (default) +"Int8" // 8-bit quantized embeddings +"Binary" // 1-bit (binary) quantized embeddings +"Text" // text input (embeddings generated from text) +``` + + +--- + +**Methods available on value factory object**: + + +```js +// Defines the queried text(s) +// =========================== +factory.byText(text); +factory.byTexts(textsArray); + +// Defines the queried text(s) and the embedding generation task to use. +// These overloads should be used only when querying a static-index where vector fields contain +// numerical embeddings that were not generated by RavenDB's built-in embedding tasks. +// The text is embedded at query time using the specified task ID and compared to the indexed vectors. +factory.byText(text, embeddingsGenerationTaskIdentifier); +factory.byTexts(textsArray, embeddingsGenerationTaskIdentifier); + +// Query by the embedding(s) indexed from the specified document for the queried field +factory.forDocument(documentId); + +// Define the queried embedding: +// ============================= + +factory.byEmbedding(embedding); +// where embedding is either: +// - number[] (single embedding) +// - RavenVector([...]) (wrapped vector object) + +factory.byEmbeddings(embeddings); +// where embeddings is: +// - number[][] (multiple embeddings) + +factory.byBase64(base64Embedding); +// where base64Embedding is: +// - string (single base64-encoded embedding) +``` + + +--- + +#### `RavenVector`: + +RavenVector is RavenDB’s dedicated representation for storing and querying numerical embeddings. +Learn more in [RavenVector](../../../ai-integration/vector-search/data-types-for-vector-search.mdx#ravenvector). + + +```js +// representation of a RavenVector: +{ "@vector": number[] } + +// Helper to create this wrapper: +RavenVector(numberArray) // => { "@vector": numberArray } +``` + + +#### `VectorQuantizer`: + +RavenDB provides the following quantizer methods. +Use them to transform your raw data to the desired format. +Other quantizers may not be compatible. + + +```js +// VectorQuantizer is exported by the package and exposes these methods: + +VectorQuantizer.toInt8(rawEmbedding); // number[] | Float32Array -> number[] +VectorQuantizer.toInt1(rawEmbedding); // number[] | Float32Array -> number[] +``` + diff --git a/docs/ai-integration/vector-search/vector-search-using-dynamic-query.mdx b/docs/ai-integration/vector-search/vector-search-using-dynamic-query.mdx index 07737df33a..0e9af95f14 100644 --- a/docs/ai-integration/vector-search/vector-search-using-dynamic-query.mdx +++ b/docs/ai-integration/vector-search/vector-search-using-dynamic-query.mdx @@ -8,8 +8,9 @@ import LanguageSwitcher from "@site/src/components/LanguageSwitcher"; import LanguageContent from "@site/src/components/LanguageContent"; import VectorSearchUsingDynamicQueryCsharp from './content/_vector-search-using-dynamic-query-csharp.mdx'; +import VectorSearchUsingDynamicQueryNodejs from './content/_vector-search-using-dynamic-query-nodejs.mdx'; -export const supportedLanguages = ["csharp"]; +export const supportedLanguages = ["csharp", "nodejs"]; @@ -17,6 +18,10 @@ export const supportedLanguages = ["csharp"]; + + + +