mirror of
https://github.com/vxcontrol/langchaingo.git
synced 2026-07-21 00:45:22 -04:00
vectorstores: Add azureaisearch (#462)
* add azure ai search as vectorstore
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tmc/langchaingo/embeddings"
|
||||
"github.com/tmc/langchaingo/schema"
|
||||
"github.com/tmc/langchaingo/vectorstores"
|
||||
)
|
||||
|
||||
// Store is a wrapper to use azure AI search rest API.
|
||||
type Store struct {
|
||||
azureAISearchEndpoint string
|
||||
azureAISearchAPIKey string
|
||||
embedder embeddings.Embedder
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNumberOfVectorDoesNotMatch when providing documents,
|
||||
// the number of vectors generated should be equal to the number of docs.
|
||||
ErrNumberOfVectorDoesNotMatch = errors.New(
|
||||
"number of vectors from embedder does not match number of documents",
|
||||
)
|
||||
// ErrAssertingMetadata SearchScore is stored as float64.
|
||||
ErrAssertingSearchScore = errors.New(
|
||||
"couldn't assert @search.score to float64",
|
||||
)
|
||||
// ErrAssertingMetadata Metadata is stored as string.
|
||||
ErrAssertingMetadata = errors.New(
|
||||
"couldn't assert metadata to string",
|
||||
)
|
||||
// ErrAssertingContent Content is stored as string.
|
||||
ErrAssertingContent = errors.New(
|
||||
"couldn't assert content to string",
|
||||
)
|
||||
)
|
||||
|
||||
// New creates a vectorstore for azure AI search
|
||||
// and returns the `Store` object needed by the other accessors.
|
||||
func New(opts ...Option) (Store, error) {
|
||||
s := Store{
|
||||
client: http.DefaultClient,
|
||||
}
|
||||
|
||||
if err := applyClientOptions(&s, opts...); err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _ vectorstores.VectorStore = &Store{}
|
||||
|
||||
// AddDocuments adds the text and metadata from the documents to the Chroma collection associated with 'Store'.
|
||||
// and returns the ids of the added documents.
|
||||
func (s *Store) AddDocuments(
|
||||
ctx context.Context,
|
||||
docs []schema.Document,
|
||||
options ...vectorstores.Option,
|
||||
) ([]string, error) {
|
||||
opts := s.getOptions(options...)
|
||||
ids := []string{}
|
||||
|
||||
texts := []string{}
|
||||
|
||||
for _, doc := range docs {
|
||||
texts = append(texts, doc.PageContent)
|
||||
}
|
||||
|
||||
vectors, err := s.embedder.EmbedDocuments(ctx, texts)
|
||||
if err != nil {
|
||||
return ids, err
|
||||
}
|
||||
|
||||
if len(vectors) != len(docs) {
|
||||
return ids, ErrNumberOfVectorDoesNotMatch
|
||||
}
|
||||
for i, doc := range docs {
|
||||
id := uuid.NewString()
|
||||
if err = s.UploadDocument(ctx, id, opts.NameSpace, doc.PageContent, vectors[i], doc.Metadata); err != nil {
|
||||
return ids, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// SimilaritySearch creates a vector embedding from the query using the embedder
|
||||
// and queries to find the most similar documents.
|
||||
func (s *Store) SimilaritySearch(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
numDocuments int,
|
||||
options ...vectorstores.Option,
|
||||
) ([]schema.Document, error) {
|
||||
opts := s.getOptions(options...)
|
||||
|
||||
queryVector, err := s.embedder.EmbedQuery(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := SearchDocumentsRequestInput{
|
||||
Vectors: []SearchDocumentsRequestInputVector{{
|
||||
Fields: "contentVector",
|
||||
Value: queryVector,
|
||||
K: numDocuments,
|
||||
}},
|
||||
}
|
||||
|
||||
if filter, ok := opts.Filters.(string); ok {
|
||||
payload.Filter = filter
|
||||
}
|
||||
|
||||
searchResults := SearchDocumentsRequestOuput{}
|
||||
if err := s.SearchDocuments(ctx, opts.NameSpace, payload, &searchResults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := []schema.Document{}
|
||||
for _, searchResult := range searchResults.Value {
|
||||
doc, err := assertResultValues(searchResult)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
if opts.ScoreThreshold > 0 && opts.ScoreThreshold > doc.Score {
|
||||
continue
|
||||
}
|
||||
|
||||
output = append(output, *doc)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func assertResultValues(searchResult map[string]interface{}) (*schema.Document, error) {
|
||||
var score float32
|
||||
if scoreFloat64, ok := searchResult["@search.score"].(float64); ok {
|
||||
score = float32(scoreFloat64)
|
||||
} else {
|
||||
return nil, ErrAssertingSearchScore
|
||||
}
|
||||
|
||||
metadata := map[string]interface{}{}
|
||||
if resultMetadata, ok := searchResult["metadata"].(string); ok {
|
||||
if err := json.Unmarshal([]byte(resultMetadata), &metadata); err != nil {
|
||||
return nil, fmt.Errorf("couldn't unmarshall metadata %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, ErrAssertingMetadata
|
||||
}
|
||||
|
||||
var pageContent string
|
||||
var ok bool
|
||||
if pageContent, ok = searchResult["content"].(string); !ok {
|
||||
return nil, ErrAssertingContent
|
||||
}
|
||||
|
||||
return &schema.Document{
|
||||
PageContent: pageContent,
|
||||
Metadata: metadata,
|
||||
Score: score,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package azureaisearch_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tmc/langchaingo/chains"
|
||||
"github.com/tmc/langchaingo/embeddings"
|
||||
"github.com/tmc/langchaingo/llms/openai"
|
||||
"github.com/tmc/langchaingo/schema"
|
||||
"github.com/tmc/langchaingo/vectorstores"
|
||||
"github.com/tmc/langchaingo/vectorstores/azureaisearch"
|
||||
)
|
||||
|
||||
func checkEnvVariables(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
azureaisearchEndpoint := os.Getenv(azureaisearch.EnvironmentVariableEndpoint)
|
||||
if azureaisearchEndpoint == "" {
|
||||
t.Skipf("Must set %s to run test", azureaisearch.EnvironmentVariableEndpoint)
|
||||
}
|
||||
|
||||
azureaisearchAPIKey := os.Getenv(azureaisearch.EnvironmentVariableAPIKey)
|
||||
if azureaisearchAPIKey == "" {
|
||||
t.Skipf("Must set %s to run test", azureaisearch.EnvironmentVariableAPIKey)
|
||||
}
|
||||
|
||||
if openaiKey := os.Getenv("OPENAI_API_KEY"); openaiKey == "" {
|
||||
t.Skip("OPENAI_API_KEY not set")
|
||||
}
|
||||
}
|
||||
|
||||
func setIndex(t *testing.T, storer azureaisearch.Store, indexName string) {
|
||||
t.Helper()
|
||||
if err := storer.CreateIndex(context.TODO(), indexName); err != nil {
|
||||
t.Fatalf("error creating index: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeIndex(t *testing.T, storer azureaisearch.Store, indexName string) {
|
||||
t.Helper()
|
||||
if err := storer.DeleteIndex(context.TODO(), indexName); err != nil {
|
||||
t.Fatalf("error deleting index: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setLLM(t *testing.T) *openai.LLM {
|
||||
t.Helper()
|
||||
openaiOpts := []openai.Option{}
|
||||
|
||||
if openAIBaseURL := os.Getenv("OPENAI_BASE_URL"); openAIBaseURL != "" {
|
||||
openaiOpts = append(openaiOpts,
|
||||
openai.WithBaseURL(openAIBaseURL),
|
||||
openai.WithAPIType(openai.APITypeAzure),
|
||||
openai.WithEmbeddingModel("text-embedding-ada-002"),
|
||||
openai.WithModel("gpt-4"),
|
||||
)
|
||||
}
|
||||
|
||||
llm, err := openai.New(openaiOpts...)
|
||||
if err != nil {
|
||||
t.Fatalf("error setting openAI embedded: %v\n", err)
|
||||
}
|
||||
|
||||
return llm
|
||||
}
|
||||
|
||||
func TestAzureaiSearchStoreRest(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkEnvVariables(t)
|
||||
indexName := uuid.New().String()
|
||||
|
||||
llm := setLLM(t)
|
||||
e, err := embeddings.NewEmbedder(llm)
|
||||
require.NoError(t, err)
|
||||
|
||||
storer, err := azureaisearch.New(
|
||||
azureaisearch.WithEmbedder(e),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
setIndex(t, storer, indexName)
|
||||
defer removeIndex(t, storer, indexName)
|
||||
|
||||
_, err = storer.AddDocuments(context.Background(), []schema.Document{
|
||||
{PageContent: "tokyo"},
|
||||
{PageContent: "potato"},
|
||||
}, vectorstores.WithNameSpace(indexName))
|
||||
require.NoError(t, err)
|
||||
|
||||
docs, err := storer.SimilaritySearch(context.Background(), "japan", 1, vectorstores.WithNameSpace(indexName))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, docs, 1)
|
||||
require.Equal(t, "tokyo", docs[0].PageContent)
|
||||
}
|
||||
|
||||
func TestAzureaiSearchStoreRestWithScoreThreshold(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkEnvVariables(t)
|
||||
indexName := uuid.New().String()
|
||||
|
||||
llm := setLLM(t)
|
||||
e, err := embeddings.NewEmbedder(llm)
|
||||
require.NoError(t, err)
|
||||
|
||||
storer, err := azureaisearch.New(
|
||||
azureaisearch.WithEmbedder(e),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
setIndex(t, storer, indexName)
|
||||
defer removeIndex(t, storer, indexName)
|
||||
|
||||
_, err = storer.AddDocuments(context.Background(), []schema.Document{
|
||||
{PageContent: "Tokyo"},
|
||||
{PageContent: "Yokohama"},
|
||||
{PageContent: "Osaka"},
|
||||
{PageContent: "Nagoya"},
|
||||
{PageContent: "Sapporo"},
|
||||
{PageContent: "Fukuoka"},
|
||||
{PageContent: "Dublin"},
|
||||
{PageContent: "Paris"},
|
||||
{PageContent: "London "},
|
||||
{PageContent: "New York"},
|
||||
}, vectorstores.WithNameSpace(indexName))
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second)
|
||||
// test with a score threshold of 0.84, expected 6 documents
|
||||
docs, err := storer.SimilaritySearch(context.Background(),
|
||||
"Which of these are cities in Japan", 10,
|
||||
vectorstores.WithScoreThreshold(0.84),
|
||||
vectorstores.WithNameSpace(indexName))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, docs, 6)
|
||||
}
|
||||
|
||||
func TestAzureaiSearchAsRetriever(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkEnvVariables(t)
|
||||
indexName := uuid.New().String()
|
||||
|
||||
llm := setLLM(t)
|
||||
e, err := embeddings.NewEmbedder(llm)
|
||||
require.NoError(t, err)
|
||||
|
||||
storer, err := azureaisearch.New(
|
||||
azureaisearch.WithEmbedder(e),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
setIndex(t, storer, indexName)
|
||||
defer removeIndex(t, storer, indexName)
|
||||
|
||||
_, err = storer.AddDocuments(
|
||||
context.Background(),
|
||||
[]schema.Document{
|
||||
{PageContent: "The color of the house is blue."},
|
||||
{PageContent: "The color of the car is red."},
|
||||
{PageContent: "The color of the desk is orange."},
|
||||
},
|
||||
vectorstores.WithNameSpace(indexName),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
result, err := chains.Run(
|
||||
context.TODO(),
|
||||
chains.NewRetrievalQAFromLLM(
|
||||
llm,
|
||||
vectorstores.ToRetriever(&storer, 1, vectorstores.WithNameSpace(indexName)),
|
||||
),
|
||||
"What color is the desk?",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(result, "orange"), "expected orange in result")
|
||||
}
|
||||
|
||||
func TestAzureaiSearchAsRetrieverWithScoreThreshold(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkEnvVariables(t)
|
||||
indexName := uuid.New().String()
|
||||
|
||||
llm := setLLM(t)
|
||||
e, err := embeddings.NewEmbedder(llm)
|
||||
require.NoError(t, err)
|
||||
|
||||
storer, err := azureaisearch.New(
|
||||
azureaisearch.WithEmbedder(e),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
setIndex(t, storer, indexName)
|
||||
defer removeIndex(t, storer, indexName)
|
||||
|
||||
_, err = storer.AddDocuments(
|
||||
context.Background(),
|
||||
[]schema.Document{
|
||||
{PageContent: "The color of the house is blue."},
|
||||
{PageContent: "The color of the car is red."},
|
||||
{PageContent: "The color of the desk is orange."},
|
||||
{PageContent: "The color of the lamp beside the desk is black."},
|
||||
{PageContent: "The color of the chair beside the desk is beige."},
|
||||
},
|
||||
vectorstores.WithNameSpace(indexName),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second)
|
||||
result, err := chains.Run(
|
||||
context.TODO(),
|
||||
chains.NewRetrievalQAFromLLM(
|
||||
llm,
|
||||
vectorstores.ToRetriever(&storer, 5,
|
||||
vectorstores.WithNameSpace(indexName),
|
||||
vectorstores.WithScoreThreshold(0.8)),
|
||||
),
|
||||
"What colors is each piece of furniture next to the desk?",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, result, "black", "expected black in result")
|
||||
require.Contains(t, result, "beige", "expected beige in result")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package azureaisearch contains an implementation of the VectorStore interface that connects to Azure AI search.
|
||||
package azureaisearch
|
||||
@@ -0,0 +1,77 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type document struct {
|
||||
SearchAction string `json:"@search.action"`
|
||||
FieldsID string `json:"id"`
|
||||
FieldsContent string `json:"content"`
|
||||
FieldsContentVector []float32 `json:"contentVector"`
|
||||
FieldsMetadata string `json:"metadata"`
|
||||
}
|
||||
|
||||
// UploadDocument format document for similiraty search and upload it.
|
||||
func (s *Store) UploadDocument(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
indexName string,
|
||||
text string,
|
||||
vector []float32,
|
||||
metadata map[string]any,
|
||||
) error {
|
||||
metadataString, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
document := document{
|
||||
SearchAction: "upload",
|
||||
FieldsID: id,
|
||||
FieldsContent: text,
|
||||
FieldsContentVector: vector,
|
||||
FieldsMetadata: string(metadataString),
|
||||
}
|
||||
|
||||
return s.UploadDocumentAPIRequest(ctx, indexName, document)
|
||||
}
|
||||
|
||||
// UploadDocumentAPIRequest makes a request to azure AI search to upload a document.
|
||||
// tech debt: should use SDK when available: https://azure.github.io/azure-sdk/releases/latest/go.html
|
||||
func (s *Store) UploadDocumentAPIRequest(ctx context.Context, indexName string, document any) error {
|
||||
URL := fmt.Sprintf("%s/indexes/%s/docs/index?api-version=2020-06-30", s.azureAISearchEndpoint, indexName)
|
||||
|
||||
documentMap := map[string]interface{}{}
|
||||
err := structToMap(document, &documentMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err converting document struc to map: %w", err)
|
||||
}
|
||||
|
||||
documentMap["@search.action"] = "mergeOrUpload"
|
||||
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"value": []map[string]interface{}{
|
||||
documentMap,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("err marshalling body for azure ai search: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for azure ai search upload document: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
|
||||
return s.httpDefaultSend(req, "azure ai search upload document", nil)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// QueryType pseudo enum for SearchDocumentsRequestInput queryType property.
|
||||
type QueryType string
|
||||
|
||||
const (
|
||||
QueryTypeSimple QueryType = "simple"
|
||||
QueryTypeFull QueryType = "full"
|
||||
QueryTypeSemantic QueryType = "semantic"
|
||||
)
|
||||
|
||||
// QueryCaptions pseudo enum for SearchDocumentsRequestInput queryCaptions property.
|
||||
type QueryCaptions string
|
||||
|
||||
const (
|
||||
QueryTypeExtractive QueryCaptions = "extractive"
|
||||
QueryTypeNone QueryCaptions = "none"
|
||||
)
|
||||
|
||||
// SpellerType pseudo enum for SearchDocumentsRequestInput spellerType property.
|
||||
type SpellerType string
|
||||
|
||||
const (
|
||||
SpellerTypeLexicon SpellerType = "lexicon"
|
||||
SpellerTypeNone SpellerType = "none"
|
||||
)
|
||||
|
||||
// SearchDocumentsRequestInput is the input struct to format a payload in order to search for a document.
|
||||
type SearchDocumentsRequestInput struct {
|
||||
Count bool `json:"count,omitempty"`
|
||||
Captions QueryCaptions `json:"captions,omitempty"`
|
||||
Facets []string `json:"facets,omitempty"`
|
||||
Filter string `json:"filter,omitempty"`
|
||||
Highlight string `json:"highlight,omitempty"`
|
||||
HighlightPostTag string `json:"highlightPostTag,omitempty"`
|
||||
HighlightPreTag string `json:"highlightPreTag,omitempty"`
|
||||
MinimumCoverage int16 `json:"minimumCoverage,omitempty"`
|
||||
Orderby string `json:"orderby,omitempty"`
|
||||
QueryType QueryType `json:"queryType,omitempty"`
|
||||
QueryLanguage string `json:"queryLanguage,omitempty"`
|
||||
Speller SpellerType `json:"speller,omitempty"`
|
||||
SemanticConfiguration string `json:"semanticConfiguration,omitempty"`
|
||||
ScoringParameters []string `json:"scoringParameters,omitempty"`
|
||||
ScoringProfile string `json:"scoringProfile,omitempty"`
|
||||
Search string `json:"search,omitempty"`
|
||||
SearchFields string `json:"searchFields,omitempty"`
|
||||
SearchMode string `json:"searchMode,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
ScoringStatistics string `json:"scoringStatistics,omitempty"`
|
||||
Select string `json:"select,omitempty"`
|
||||
Skip int `json:"skip,omitempty"`
|
||||
Top int `json:"top,omitempty"`
|
||||
Vectors []SearchDocumentsRequestInputVector `json:"vectors,omitempty"`
|
||||
VectorFilterMode string `json:"vectorFilterMode,omitempty"`
|
||||
}
|
||||
|
||||
// SearchDocumentsRequestInputVector is the input struct for vector search.
|
||||
type SearchDocumentsRequestInputVector struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Value []float32 `json:"value,omitempty"`
|
||||
Fields string `json:"fields,omitempty"`
|
||||
K int `json:"k,omitempty"`
|
||||
Exhaustive bool `json:"exhaustive,omitempty"`
|
||||
}
|
||||
|
||||
// SearchDocumentsRequestOuput is the output struct for search.
|
||||
type SearchDocumentsRequestOuput struct {
|
||||
OdataCount int `json:"@odata.count,omitempty"`
|
||||
SearchFacets struct {
|
||||
Category []struct {
|
||||
Count int `json:"count,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
} `json:"category,omitempty"`
|
||||
} `json:"@search.facets,omitempty"`
|
||||
SearchNextPageParameters SearchDocumentsRequestInput `json:"@search.nextPageParameters,omitempty"`
|
||||
Value []map[string]interface{} `json:"value,omitempty"`
|
||||
OdataNextLink string `json:"@odata.nextLink,omitempty"`
|
||||
}
|
||||
|
||||
// SearchDocuments send a request to azure AI search Rest API for searching documents.
|
||||
func (s *Store) SearchDocuments(
|
||||
ctx context.Context,
|
||||
indexName string,
|
||||
payload SearchDocumentsRequestInput,
|
||||
output *SearchDocumentsRequestOuput,
|
||||
) error {
|
||||
URL := fmt.Sprintf("%s/indexes/%s/docs/search?api-version=2023-07-01-Preview", s.azureAISearchEndpoint, indexName)
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err marshalling document for azure ai search: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for azure ai search document: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
return s.httpDefaultSend(req, "search documents on azure ai search", output)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func structToMap(input any, output *map[string]interface{}) error {
|
||||
inrec, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshalling StructToMap input : %w", err)
|
||||
}
|
||||
|
||||
return json.Unmarshal(inrec, output)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ErrSendingRequest basic error when the request failed.
|
||||
var ErrSendingRequest = errors.New(
|
||||
"error sedding request",
|
||||
)
|
||||
|
||||
func (s *Store) httpDefaultSend(req *http.Request, serviceName string, output any) error {
|
||||
response, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err sending request for %s: %w", serviceName, err)
|
||||
}
|
||||
|
||||
return httpReadBody(response, serviceName, output)
|
||||
}
|
||||
|
||||
func httpReadBody(response *http.Response, serviceName string, output any) error {
|
||||
defer response.Body.Close()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err can't read response for %s: %w", serviceName, err)
|
||||
}
|
||||
|
||||
if output != nil {
|
||||
if err := json.Unmarshal(body, output); err != nil {
|
||||
return fmt.Errorf("err unmarshal body for %s: %w", serviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
if output != nil {
|
||||
return json.Unmarshal(body, output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("error returned from %s | Status : %s | Status Code: %d | body: %s %w",
|
||||
serviceName,
|
||||
response.Status,
|
||||
response.StatusCode,
|
||||
string(body),
|
||||
ErrSendingRequest,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// IndexOption is used to customise the index when creating the index
|
||||
// useful if you use differemt embedder than text-embedding-ada-002.
|
||||
type IndexOption func(indexMap *map[string]interface{})
|
||||
|
||||
const (
|
||||
vectorDimension = 1536
|
||||
hnswParametersM = 4
|
||||
hnswParametersEfConstruction = 400
|
||||
hnswParametersEfSearch = 500
|
||||
)
|
||||
|
||||
// CreateIndex defines a default index (default one is made for text-embedding-ada-002)
|
||||
// but can be customised through IndexOption functions.
|
||||
func (s *Store) CreateIndex(ctx context.Context, indexName string, opts ...IndexOption) error {
|
||||
defaultIndex := map[string]interface{}{
|
||||
"name": indexName,
|
||||
"fields": []map[string]interface{}{
|
||||
{
|
||||
"key": true,
|
||||
"name": "id",
|
||||
"type": FieldTypeString,
|
||||
"filterable": true,
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"type": FieldTypeString,
|
||||
"searchable": true,
|
||||
},
|
||||
{
|
||||
"name": "contentVector",
|
||||
"type": CollectionField(FieldTypeSingle),
|
||||
"searchable": true,
|
||||
// dimensions is the number of dimensions generated by the embedding model. For text-embedding-ada-002, it's 1536.
|
||||
// basically the length of the array returned by the function
|
||||
"dimensions": vectorDimension,
|
||||
"vectorSearchProfile": "default",
|
||||
},
|
||||
{
|
||||
"name": "metadata",
|
||||
"type": FieldTypeString,
|
||||
"searchable": true,
|
||||
},
|
||||
},
|
||||
"vectorSearch": map[string]interface{}{
|
||||
"algorithms": []map[string]interface{}{
|
||||
{
|
||||
"name": "default-hnsw",
|
||||
"kind": "hnsw",
|
||||
"hnswParameters": map[string]interface{}{
|
||||
"m": hnswParametersM,
|
||||
"efConstruction": hnswParametersEfConstruction,
|
||||
"efSearch": hnswParametersEfSearch,
|
||||
"metric": "cosine",
|
||||
},
|
||||
},
|
||||
},
|
||||
"profiles": []map[string]interface{}{
|
||||
{
|
||||
"name": "default",
|
||||
"algorithm": "default-hnsw",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, indexOption := range opts {
|
||||
indexOption(&defaultIndex)
|
||||
}
|
||||
|
||||
if err := s.CreateIndexAPIRequest(ctx, indexName, defaultIndex); err != nil {
|
||||
return fmt.Errorf("error creating index: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIndexAPIRequest send a request to azure AI search Rest API for creating an index.
|
||||
func (s *Store) CreateIndexAPIRequest(ctx context.Context, indexName string, payload any) error {
|
||||
URL := fmt.Sprintf("%s/indexes/%s?api-version=2023-11-01", s.azureAISearchEndpoint, indexName)
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err marshalling json: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, URL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for index creating: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
|
||||
if err := s.httpDefaultSend(req, "index creating for azure ai search", nil); err != nil {
|
||||
return fmt.Errorf("err request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CreateIndexAPIRequest send a request to azure AI search Rest API for deleting an index.
|
||||
func (s *Store) DeleteIndex(ctx context.Context, indexName string) error {
|
||||
URL := fmt.Sprintf("%s/indexes/%s?api-version=2023-11-01", s.azureAISearchEndpoint, indexName)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, URL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for index creating: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
|
||||
if err := s.httpDefaultSend(req, "index creating for azure ai search", nil); err != nil {
|
||||
return fmt.Errorf("err request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ListIndexes send a request to azure AI search Rest API for creatin an index, helper function.
|
||||
func (s *Store) ListIndexes(ctx context.Context, output *map[string]interface{}) error {
|
||||
URL := fmt.Sprintf("%s/indexes?api-version=2023-11-01", s.azureAISearchEndpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for index retrieving: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
|
||||
return s.httpDefaultSend(req, "search documents on azure ai search", output)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RetrieveIndex send a request to azure AI search Rest API for retrieving an index, helper function.
|
||||
func (s *Store) RetrieveIndex(ctx context.Context, indexName string, output *map[string]interface{}) error {
|
||||
URL := fmt.Sprintf("%s/indexes/%s?api-version=2023-11-01", s.azureAISearchEndpoint, indexName)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("err setting request for index retrieving: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if s.azureAISearchAPIKey != "" {
|
||||
req.Header.Add("api-key", s.azureAISearchAPIKey)
|
||||
}
|
||||
|
||||
return s.httpDefaultSend(req, "search documents on azure ai search", output)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/embeddings"
|
||||
"github.com/tmc/langchaingo/vectorstores"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvironmentVariableEndpoint environment variable to set azure ai search endpoint.
|
||||
EnvironmentVariableEndpoint string = "AZURE_AI_SEARCH_ENDPOINT"
|
||||
// EnvironmentVariableAPIKey environment variable to set azure ai api key.
|
||||
EnvironmentVariableAPIKey string = "AZURE_AI_SEARCH_API_KEY"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingEnvVariableAzureAISearchEndpoint environment variable to set azure ai search endpoint missing.
|
||||
ErrMissingEnvVariableAzureAISearchEndpoint = errors.New(
|
||||
"missing azureAISearchEndpoint",
|
||||
)
|
||||
// ErrMissingEmbedded embedder is missing, one should be set when instantiating the vectorstore.
|
||||
ErrMissingEmbedded = errors.New(
|
||||
"missing embedder",
|
||||
)
|
||||
)
|
||||
|
||||
func (s *Store) getOptions(options ...vectorstores.Option) vectorstores.Options {
|
||||
opts := vectorstores.Options{}
|
||||
for _, opt := range options {
|
||||
opt(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// WithFilters can set the filter property in search document payload.
|
||||
func WithFilters(filters any) vectorstores.Option {
|
||||
return func(o *vectorstores.Options) {
|
||||
o.Filters = filters
|
||||
}
|
||||
}
|
||||
|
||||
// Option is a function type that can be used to modify the client.
|
||||
type Option func(p *Store)
|
||||
|
||||
// WithEmbedder is an option for setting the embedder to use.
|
||||
func WithEmbedder(e embeddings.Embedder) Option {
|
||||
return func(p *Store) {
|
||||
p.embedder = e
|
||||
}
|
||||
}
|
||||
|
||||
// WithEmbedder is an option for setting the http client, the vectorstore uses the REST API,
|
||||
// default http client is set but can be overridden by this option.
|
||||
func WithHTTPClient(client *http.Client) Option {
|
||||
return func(s *Store) {
|
||||
s.client = client
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIKey is an option for setting the azure AI search API Key.
|
||||
func WithAPIKey(azureAISearchAPIKey string) Option {
|
||||
return func(s *Store) {
|
||||
s.azureAISearchAPIKey = azureAISearchAPIKey
|
||||
}
|
||||
}
|
||||
|
||||
func applyClientOptions(s *Store, opts ...Option) error {
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
if s.azureAISearchEndpoint == "" {
|
||||
s.azureAISearchEndpoint = strings.TrimSuffix(os.Getenv(EnvironmentVariableEndpoint), "/")
|
||||
}
|
||||
|
||||
if s.azureAISearchEndpoint == "" {
|
||||
return ErrMissingEnvVariableAzureAISearchEndpoint
|
||||
}
|
||||
|
||||
if s.embedder == nil {
|
||||
return ErrMissingEmbedded
|
||||
}
|
||||
|
||||
if envVariableAPIKey := os.Getenv(EnvironmentVariableAPIKey); envVariableAPIKey != "" {
|
||||
s.azureAISearchAPIKey = envVariableAPIKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package azureaisearch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FieldType type for pseudo enum.
|
||||
type FieldType = string
|
||||
|
||||
// Pseudo enum for all the different FieldType.
|
||||
const (
|
||||
FieldTypeString FieldType = "Edm.String"
|
||||
FieldTypeSingle FieldType = "Edm.Single"
|
||||
FieldTypeInt32 FieldType = "Edm.Int32"
|
||||
FieldTypeInt64 FieldType = "Edm.Int64"
|
||||
FieldTypeDouble FieldType = "Edm.Double"
|
||||
FieldTypeBoolean FieldType = "Edm.Boolean"
|
||||
FieldTypeDatetimeOffset FieldType = "Edm.DateTimeOffset"
|
||||
FieldTypeComplexType FieldType = "Edm.ComplexType"
|
||||
)
|
||||
|
||||
// CollectionField allows to define a fieldtype as a collection.
|
||||
func CollectionField(fieldType FieldType) FieldType {
|
||||
return fmt.Sprintf("Collection(%s)", fieldType)
|
||||
}
|
||||
Reference in New Issue
Block a user