Dense vectors and kNN search
When working with vector embeddings for semantic search or machine learning applications, the Go client provides support for dense vector indexing and k-nearest neighbors (kNN) search. For the full kNN search reference, see kNN search in the Elasticsearch documentation.
Before indexing documents with vectors, create an index with the appropriate dense vector mapping.
mapping := `{
"mappings": {
"properties": {
"docid": { "type": "keyword" },
"title": { "type": "text" },
"emb": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "cosine"
}
}
}
}`
res, err := client.Indices.Create(
"my-vectors",
client.Indices.Create.WithBody(strings.NewReader(mapping)),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
- The name of the index to create.
- Pass the full mapping as a JSON body, including dense vector configuration.
similarity := densevectorsimilarity.Cosine
res, err := es.Indices.
Create("my-vectors").
Request(&create.Request{
Mappings: &types.TypeMapping{
Properties: map[string]types.Property{
"docid": types.NewKeywordProperty(),
"title": types.NewTextProperty(),
"emb": types.DenseVectorProperty{
Dims: some.Int(1536),
Index: some.Bool(true),
Similarity: &similarity,
},
},
},
}).
Do(context.Background())
- Define the vector field as a
DenseVectorProperty. - Specify the dimensionality of your vectors (e.g., 1536 for OpenAI embeddings).
- Enable indexing to support kNN search capabilities.
- Set the similarity metric:
Cosine,DotProduct, orL2Norm.
The esdsl mapping builders provide a concise, fluent syntax:
import "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
mappings := esdsl.NewTypeMapping().
AddProperty("docid", esdsl.NewKeywordProperty()).
AddProperty("title", esdsl.NewTextProperty()).
AddProperty("emb", esdsl.NewDenseVectorProperty().
Dims(1536).
Index(true).
Similarity(densevectorsimilarity.Cosine))
res, err := es.Indices.
Create("my-vectors").
Mappings(mappings).
Do(context.Background())
- Define the vector field as a
DenseVectorProperty. - Specify the dimensionality of your vectors (e.g., 1536 for OpenAI embeddings).
- Enable indexing to support kNN search capabilities.
- Set the similarity metric:
Cosine,DotProduct, orL2Norm. - Pass the mapping builder directly - no
Requestwrapper needed.
doc := `{
"docid": "doc1",
"title": "Example document with vector embedding",
"emb": [0.1, 0.2, 0.3, 0.4, 0.5]
}`
res, err := client.Index(
"my-vectors",
strings.NewReader(doc),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
- The target index.
- Vectors are passed as JSON arrays of floats.
The types.DenseVectorF32 type automatically encodes []float32 vectors as base64 strings during JSON serialization, reducing payload size and improving indexing speed:
type Document struct {
DocID string `json:"docid"`
Title string `json:"title"`
Emb types.DenseVectorF32 `json:"emb"`
}
document := Document{
DocID: "doc1",
Title: "Example document with vector embedding",
Emb: types.DenseVectorF32{0.1, 0.2, 0.3, 0.4, 0.5},
}
res, err := es.Index("my-vectors").
Request(document).
Do(context.Background())
- Use
types.DenseVectorF32instead of[]float32for vector fields. - Assign float32 slices directly; base64 encoding happens automatically during serialization.
Once your vectors are indexed, you can perform k-nearest neighbors (kNN) search to find similar documents:
query := `{
"query": {
"knn": {
"field": "emb",
"query_vector": [0.1, 0.2, 0.3, 0.4, 0.5],
"k": 10,
"num_candidates": 100
}
}
}`
res, err := client.Search(
client.Search.WithIndex("my-vectors"),
client.Search.WithBody(strings.NewReader(query)),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
- The index containing your vectors.
- Pass the kNN query as raw JSON.
queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
k := 10
numCandidates := 100
res, err := es.Search().
Index("my-vectors").
Request(&search.Request{
Query: &types.Query{
Knn: &types.KnnQuery{
Field: "emb",
QueryVector: queryVector,
K: &k,
NumCandidates: &numCandidates,
},
},
}).
Do(context.Background())
- Define your query vector (typically from the same embedding model).
- Build the kNN query using the
KnnQuerystruct. - Specify which vector field to search.
- Provide the query vector to find similar vectors.
- Return the top 10 nearest neighbors.
- Consider 100 candidates during the search for better accuracy.
The esdsl query builders provide a fluent syntax for kNN search:
queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
res, err := es.Search().
Index("my-vectors").
Query(
esdsl.NewKnnQuery().
Field("emb").
QueryVector(queryVector...).
K(10).
NumCandidates(100),
).
Do(context.Background())
- Define your query vector (typically from the same embedding model).
- Use
esdsl.NewKnnQuery()to build a kNN query with a fluent chain. - Specify which vector field to search.
- Provide the query vector to find similar vectors.
- Return the top 10 nearest neighbors.
- Consider 100 candidates during the search for better accuracy.
Using types.DenseVectorF32 (typed API) provides significant performance improvements over standard JSON arrays of floats:
- Reduced payload size: base64 encoding is more compact than JSON number arrays
- Faster parsing: Eliminates JSON number parsing overhead
- Improved indexing speed: Performance gains increase with vector dimensionality and can improve indexing speeds by up to 3x
For best performance, use types.DenseVectorF32 when your vectors are already in []float32 format. If you have pre-encoded bytes, use types.DenseVectorBytes to avoid re-encoding.
If you already have pre-encoded vector bytes from another system, use types.DenseVectorBytes:
type Document struct {
Emb types.DenseVectorBytes `json:"emb"`
}
vectorBytes := []byte{...}
document := Document{
Emb: types.DenseVectorBytes(vectorBytes),
}
res, err := es.Index("my-vectors").
Request(document).
Do(context.Background())
- Use
types.DenseVectorByteswhen you have pre-encoded bytes. - Provide the raw byte representation of your vector data.