C3 AI Documentation Home

Vector Store

The C3 Agentic AI Platform supports pgvector, a PostgreSQL extension that stores vector embeddings and searches them by similarity. Use a vector store when your application finds items by meaning instead of by exact value. Examples include recommendation systems, image recognition, and retrieval-augmented generation (RAG), where vectors find the text that informs generated content.

On this page, you choose an index type and a distance metric, declare an entity Type that stores embeddings, and load vector data. You then build the vector index and run a similarity search. The last sections show how to tune search accuracy, prewarm frequently used tables, and fix common search errors.

Prerequisites

Before you start, check that your application meets these requirements:

  • The application stores its entity Types in PostgreSQL. A shared environment (SE) uses PostgreSQL for the SQL store by default. For the default database in each kind of environment, see the section Default databases in Specify a KV Store.

  • The pgvector extension is installed in that database. To check the installed version, run the following DbAdmin command in the C3 AI Console of your application:

    JavaScript
    DbAdmin.inst().getPgVectorVersion()

    If pgvector is not installed, ask your database administrator to install it.

Choose an index type

A vector index speeds up similarity search on large datasets by searching only part of the data. pgvector offers two index types: inverted file with flat compression (IVFFlat) and Hierarchical Navigable Small Worlds (HNSW). Both trade some recall, the share of the true nearest neighbors that a search returns, for speed. The following table compares the two index types. The build parameters are the ones you set in the index entry of the @db annotation. The search-time settings use the platform field names, with the pgvector setting names in parentheses.

IVFFlatHNSW
How it worksUses k-means clustering to divide the vectors into clusters, also called lists. Each vector belongs to the nearest cluster. A search checks only the clusters closest to the query vector.Builds a multi-layered graph that links each vector to its neighbors. A search moves through the graph toward the closest vectors.
ParametersclustersCount: the number of clusters. More clusters make each search more targeted, but increase build time and index size.neighborsCount: the maximum number of links per vector. Higher values can improve recall, but increase index size and build time. efConstruction: the candidate list size during the build. Higher values build a more thorough index more slowly.
Search-time settingprobesCount (pgvector probes): how many clusters a search checks. Higher values improve recall and slow the query.efSearch (pgvector ef_search): the candidate list size during a search. Higher values improve recall and slow the query.
Build costFaster to build and uses less memory.Takes more time and memory to build.
When to chooseChoose IVFFlat when build time and memory matter most and you can rebuild the index when the data changes significantly.Choose HNSW for better query speed at the same recall, and for data that grows gradually. HNSW is less sensitive to changes in dataset size, so it needs fewer rebuilds.

To change the search-time settings, see Tune search accuracy.

Choose a distance metric

The distance metric decides how the platform measures the closeness of two vectors. You set the metric in two places: the opClass of the index field, and the metric of each similarity search. Use the same value in both places so that searches can use the index. The following table compares the three most common metrics.

MetricValue for opClass and metricWhat it measuresWhen to choose
Euclidean distanceL2 (default)The straight-line distance between two vectors. Both the magnitude and the direction of the vectors affect the result.Choose L2 when the magnitude of vector components carries meaning, such as counts or measures. L2 also suits models that are not trained for a specific vector orientation.
Cosine similarityCOSINEThe cosine of the angle between two vectors. Only the direction of the vectors affects the result, which ranges from -1 (opposite) to 1 (same direction).Choose COSINE for text applications such as semantic search and document classification, where direction carries the meaning. COSINE also suits recommendations based on similar behavior when interaction frequency is unrelated to orientation.
Dot productINNER_PRODUCTThe sum of the products of corresponding vector components. Both the magnitude and the direction of the vectors affect the result.Choose INNER_PRODUCT for models trained to optimize dot products, such as some neural networks and matrix factorization models. INNER_PRODUCT suits collaborative filtering, where both interest and its intensity matter.

The platform also accepts L1, HAMMING, and JACCARD, and sparse vector variants of L2, INNER_PRODUCT, COSINE, and L1, such as COSINE_SPARSE_VECTOR.

Configure an entity Type to store vector embeddings

To store vectors, define an EntityType with a vector field and a vector index. The Type uses two annotations:

  • @vector on the vector field: Set dimension to the number of elements in each vector. Every vector in the field and every query vector must have this number of elements.
  • @db on the Type: Add an index entry for the vector field. Each entry has a fields array, and each field has a name (the vector field) and an optional opClass (the distance metric). opClass defaults to L2.

The index parameters you set choose the index type:

  • Set clustersCount to build an IVFFlat index.
  • Set neighborsCount, efConstruction, or neither to build an HNSW index.

If the Type declares no vector index, a similarity search compares the query vector with every row. A search without an index is an exact k-nearest neighbor (KNN) search.

The following table lists the index parameters and their defaults.

ParameterIndex typeDescriptionDefault
neighborsCountHNSWThe maximum number of links per vector in the graph (pgvector m). Must be 1 or more.16
efConstructionHNSWThe size of the candidate list during the index build (pgvector ef_construction). Must be at least twice neighborsCount.64
clustersCountIVFFlatThe number of clusters (pgvector lists). Must be 1 or more.100

The following example Type stores 768-element document embeddings in an HNSW index that uses Euclidean distance. Use your own Type and field names:

Type
/**
 * This is an example Type.
 */
@db(index = [{fields:[{name:emb, opClass:L2}], neighborsCount:16, efConstruction:40}])
entity type DocumentEmbedding {
  /**
   * The ID of the doc.
   */
  id: ~
  /**
   * The text of the doc.
   */
  text: string
  /**
   * The vector embedding.
   */
  @vector(dimension=768)
  emb: string
}

In this example, the emb field holds each vector as a string of comma-separated values in square brackets, such as '[0.12, -0.03, 0.88]'. Declare a vector field that you fill yourself as a string. The platform's own vector Type, GenaiCore.Vector, declares its field as @vector(dimension=1024) embedding: !string. Add the Type to your package and deploy it. When the platform provisions the Type, it creates the table and the vector index.

The @vector annotation also accepts sourceField and embedder. Set both parameters to have the platform generate and maintain embeddings from another field of the Type. In that case, omit dimension; the embedder provides it. Declare the field as [!float], because a string field does not receive the generated embedding. To set up an embedder, see Integrate an Embedder with Vector Store.

Load vector data

Because the vector Type is an entity Type, you load it like any other entity Type. Choose the path that matches where your vectors come from:

  • The platform generates the vectors: Configure an embedder on the vector field. The platform computes the embedding from the source field when you load the text.
  • You already have vectors: Load the vectors with any standard data integration process, or create the records directly. Each vector must have the number of elements set in dimension.
  • You compute the vectors in your own pipeline: Run your preprocessing and your embedding model in your package, then load the resulting vectors.

After you load the data, query it like any other entity Type, for example with fetch or evaluate.

Build a vector index

The platform creates the vector index when it provisions the Type. To rebuild the indexes on the table, for example after a large data load, run the following commands in the C3 AI Console of your application:

JavaScript
// Get the root table name of the vector Type
var tableName = DocumentEmbedding.meta().rootC3TableName();

// Rebuild all indexes on the table
DbAdmin.inst().rebuildIndex(tableName);

To rebuild one index only, pass its name as the second argument: DbAdmin.inst().rebuildIndex(tableName, indexName). To find index names, call DbAdmin.inst().getTableIndexes(tableName). The indexName field of each entry is the name. See Database Indexes.

Building a vector index can take a long time on large datasets. These factors decide the build time:

  • Index type: IVFFlat builds faster and uses less memory than HNSW. For details, see Choose an index type.
  • Dataset size: More vectors, and more elements per vector, take longer to index.
  • System resources: The compute power and memory available to the database change the indexing speed.

A similarity search returns the rows whose vectors are closest to a query vector. To run a search, build an EvalSpec that orders the rows by vector distance, and evaluate it on the Type. Run the following commands in the C3 AI Console of your application:

JavaScript
// Replace with your query vector. It must have the same number of elements as the emb field (768).
var queryVector = '[0.12, -0.03, 0.88, ...]';
var distanceMetric = 'L2';

// The distance between each emb vector and the query vector
var distance = "vectorDistance('emb', '" + queryVector + "', {'metric': '" + distanceMetric + "'})";

var spec = EvalSpec.builder()
                   .projection('id, text, ' + distance) // Fields to return, and the distance
                   .order(distance) // Closest vectors first
                   .limit(3) // Number of results
                   .build();

DocumentEmbedding.eval(spec);

The sample uses these values:

  • queryVector: The vector that the search compares each row with. Pass queryVector to vectorDistance as a quoted string.
  • distanceMetric: The distance metric. Use the same value as the opClass of the index. If you omit metric, the search uses L2. For the options, see Choose a distance metric.
  • projection: The fields to return. Include the distance expression, because the search orders only by expressions that the projection includes.
  • order: Sorts the rows by the distance between each vector in the emb field and queryVector, closest first.
  • limit: The number of results to return, which is the k in a top-k search.

Tune search accuracy

Two search-time settings trade query speed for recall:

  • probesCount (IVFFlat, pgvector probes): The number of clusters that a search checks.
  • efSearch (HNSW, pgvector ef_search): The size of the candidate list during a search.

If you set neither, searches use the database defaults. In pgvector, the default is 1 probe and an ef_search of 40. Choose where to set the two settings based on which searches need the change:

  • One Type: Set similaritySearchPerfParams in the @db annotation of the Type. Choose this level when every search on the Type needs the same balance. See Ann.Db.
  • One request: Set similaritySearchPerfParams in the FetchFilterSpec of a fetch or evaluate request. Choose this level when only some searches need higher recall. The EvalSpec that the similarity search sample uses has no such field, so this level does not apply to that sample.
  • The whole database: A user with the C3.ClusterAdmin role sets the parameters with DbAdmin.inst().setPgConfigParams. This level affects every table in the database.

For the parameter fields, see Db.Index.IvfFlat.SimilaritySearchPerfParams (probesCount) and Db.Index.Hnsw.SimilaritySearchPerfParams (efSearch).

The value of similaritySearchPerfParams must include a type field that names one of these two Types. The following annotation sets efSearch and iterativeScan for every search on the example Type:

Type
@db(index = [{fields:[{name:emb, opClass:L2}], neighborsCount:16, efConstruction:40}],
    similaritySearchPerfParams = {type: "Db.Index.Hnsw.SimilaritySearchPerfParams", efSearch: 100,
                                  iterativeScan: "STRICT_ORDER"})
entity type DocumentEmbedding {
  // fields as in the earlier example
}

An HNSW search collects efSearch candidates and filters them afterwards. The platform adds an application filter to every query, so a search can return fewer rows than its limit. Set iterativeScan to keep scanning the index until the search fills the limit:

  • STRICT_ORDER returns the rows in exact distance order.
  • RELAXED_ORDER is faster, but can return rows slightly out of distance order.
  • OFF turns iterative scanning off.

Two more fields cap an iterative scan. maxScanTuples limits how many index entries it reads, and scanMemMultiplier limits its memory as a multiple of the PostgreSQL work_mem setting. A single request can set maxScanTuples to at most 20000 and scanMemMultiplier to at most 1. For an IVFFlat index, iterativeScan accepts OFF or RELAXED_ORDER, and maxProbes caps the clusters the scan checks. These fields need pgvector 0.8 or later.

Prewarm frequently used tables

Prewarming loads a table or an index into the database cache before heavy reads, which reduces read latency at the start of those reads. To prewarm a table, run the following command in the C3 AI Console of your application:

JavaScript
DbAdmin.inst().pgPrewarm(tableName);

tableName is the value that DocumentEmbedding.meta().rootC3TableName() returns. You can also pass an index name. The command returns the number of blocks it loaded.

Prewarm during periods of low demand, so that the data is in the cache when the heavy reads start.

Troubleshooting

The following errors are the most common when you run a similarity search, with the cause and the fix for each.

A similarity search fails with "Invalid field"

The projection names a field that the Type does not declare. Project only fields of the Type, such as id and text in the example.

A similarity search fails with "Can't order using an expression that isn't included in the projections"

The order expression is missing from the projection. Add the same vectorDistance expression to the projection.

A similarity search fails with "different vector dimensions"

The query vector has a different number of elements than the vector field. Use a query vector with the number of elements set in dimension.

See also

Was this page helpful?