fix: replace Python CI workflow with Go CI
All checks were successful
CI / Release (push) Successful in 58s
CI / Notify (push) Successful in 1s
CI / Lint (push) Successful in 3m24s
CI / Test (push) Successful in 3m14s

- Replace uv/ruff/pytest with Go setup, golangci-lint, go test
- Library-only: lint + test + release (tag) + notify, no Docker build
This commit is contained in:
2026-02-20 08:49:29 -05:00
parent 39673d31b8
commit 9876cb9388
2 changed files with 297 additions and 292 deletions

View File

@@ -17,20 +17,22 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up uv - name: Set up Go
run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.local/bin" >> $GITHUB_PATH uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set up Python - name: Run go vet
run: uv python install 3.13 run: go vet ./...
- name: Install dependencies - name: Install golangci-lint
run: uv sync --frozen --extra dev run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b "$(go env GOPATH)/bin"
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Run ruff check - name: Run golangci-lint
run: uv run ruff check . run: golangci-lint run ./...
- name: Run ruff format check
run: uv run ruff format --check .
test: test:
name: Test name: Test
@@ -39,17 +41,20 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up uv - name: Set up Go
run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.local/bin" >> $GITHUB_PATH uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set up Python - name: Verify dependencies
run: uv python install 3.13 run: go mod verify
- name: Install dependencies - name: Build
run: uv sync --frozen --extra dev run: go build -v ./...
- name: Run tests with coverage - name: Run tests
run: uv run pytest --cov=handler_base --cov-report=xml --cov-report=term run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
release: release:
name: Release name: Release

View File

@@ -6,16 +6,16 @@
package clients package clients
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
) )
// ─── Shared transport & buffer pool ───────────────────────────────────────── // ─── Shared transport & buffer pool ─────────────────────────────────────────
@@ -23,393 +23,393 @@ import (
// SharedTransport is the process-wide HTTP transport used by every service // SharedTransport is the process-wide HTTP transport used by every service
// client. Tweak pool sizes here rather than creating per-client transports. // client. Tweak pool sizes here rather than creating per-client transports.
var SharedTransport = &http.Transport{ var SharedTransport = &http.Transport{
MaxIdleConns: 100, MaxIdleConns: 100,
MaxIdleConnsPerHost: 10, MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
DisableCompression: true, // in-cluster traffic; skip gzip overhead DisableCompression: true, // in-cluster traffic; skip gzip overhead
} }
// bufPool recycles *bytes.Buffer to avoid per-request allocations. // bufPool recycles *bytes.Buffer to avoid per-request allocations.
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) }, New: func() any { return new(bytes.Buffer) },
} }
func getBuf() *bytes.Buffer { func getBuf() *bytes.Buffer {
buf := bufPool.Get().(*bytes.Buffer) buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() buf.Reset()
return buf return buf
} }
func putBuf(buf *bytes.Buffer) { func putBuf(buf *bytes.Buffer) {
if buf.Cap() > 1<<20 { // don't cache buffers > 1 MiB if buf.Cap() > 1<<20 { // don't cache buffers > 1 MiB
return return
} }
bufPool.Put(buf) bufPool.Put(buf)
} }
// ─── httpClient base ──────────────────────────────────────────────────────── // ─── httpClient base ────────────────────────────────────────────────────────
// httpClient is the shared base for all service clients. // httpClient is the shared base for all service clients.
type httpClient struct { type httpClient struct {
client *http.Client client *http.Client
baseURL string baseURL string
} }
func newHTTPClient(baseURL string, timeout time.Duration) *httpClient { func newHTTPClient(baseURL string, timeout time.Duration) *httpClient {
return &httpClient{ return &httpClient{
client: &http.Client{ client: &http.Client{
Timeout: timeout, Timeout: timeout,
Transport: SharedTransport, Transport: SharedTransport,
}, },
baseURL: baseURL, baseURL: baseURL,
} }
} }
func (h *httpClient) postJSON(ctx context.Context, path string, body any) ([]byte, error) { func (h *httpClient) postJSON(ctx context.Context, path string, body any) ([]byte, error) {
buf := getBuf() buf := getBuf()
defer putBuf(buf) defer putBuf(buf)
if err := json.NewEncoder(buf).Encode(body); err != nil { if err := json.NewEncoder(buf).Encode(body); err != nil {
return nil, fmt.Errorf("marshal: %w", err) return nil, fmt.Errorf("marshal: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.baseURL+path, buf) req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.baseURL+path, buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
return h.do(req) return h.do(req)
} }
func (h *httpClient) get(ctx context.Context, path string, params url.Values) ([]byte, error) { func (h *httpClient) get(ctx context.Context, path string, params url.Values) ([]byte, error) {
u := h.baseURL + path u := h.baseURL + path
if len(params) > 0 { if len(params) > 0 {
u += "?" + params.Encode() u += "?" + params.Encode()
} }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return h.do(req) return h.do(req)
} }
func (h *httpClient) getRaw(ctx context.Context, path string, params url.Values) ([]byte, error) { func (h *httpClient) getRaw(ctx context.Context, path string, params url.Values) ([]byte, error) {
return h.get(ctx, path, params) return h.get(ctx, path, params)
} }
func (h *httpClient) postMultipart(ctx context.Context, path string, fieldName string, fileName string, fileData []byte, fields map[string]string) ([]byte, error) { func (h *httpClient) postMultipart(ctx context.Context, path string, fieldName string, fileName string, fileData []byte, fields map[string]string) ([]byte, error) {
buf := getBuf() buf := getBuf()
defer putBuf(buf) defer putBuf(buf)
w := multipart.NewWriter(buf) w := multipart.NewWriter(buf)
part, err := w.CreateFormFile(fieldName, fileName) part, err := w.CreateFormFile(fieldName, fileName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := part.Write(fileData); err != nil { if _, err := part.Write(fileData); err != nil {
return nil, err return nil, err
} }
for k, v := range fields { for k, v := range fields {
_ = w.WriteField(k, v) _ = w.WriteField(k, v)
} }
_ = w.Close() _ = w.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.baseURL+path, buf) req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.baseURL+path, buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Content-Type", w.FormDataContentType())
return h.do(req) return h.do(req)
} }
func (h *httpClient) do(req *http.Request) ([]byte, error) { func (h *httpClient) do(req *http.Request) ([]byte, error) {
resp, err := h.client.Do(req) resp, err := h.client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("http %s %s: %w", req.Method, req.URL.Path, err) return nil, fmt.Errorf("http %s %s: %w", req.Method, req.URL.Path, err)
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
buf := getBuf() buf := getBuf()
defer putBuf(buf) defer putBuf(buf)
if _, err := io.Copy(buf, resp.Body); err != nil { if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, fmt.Errorf("read body: %w", err) return nil, fmt.Errorf("read body: %w", err)
} }
// Return a copy so the pooled buffer can be safely recycled. // Return a copy so the pooled buffer can be safely recycled.
body := make([]byte, buf.Len()) body := make([]byte, buf.Len())
copy(body, buf.Bytes()) copy(body, buf.Bytes())
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
return nil, fmt.Errorf("http %d: %s", resp.StatusCode, string(body)) return nil, fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
} }
return body, nil return body, nil
} }
func (h *httpClient) healthCheck(ctx context.Context) bool { func (h *httpClient) healthCheck(ctx context.Context) bool {
data, err := h.get(ctx, "/health", nil) data, err := h.get(ctx, "/health", nil)
_ = data _ = data
return err == nil return err == nil
} }
// ─── Embeddings Client ────────────────────────────────────────────────────── // ─── Embeddings Client ──────────────────────────────────────────────────────
// EmbeddingsClient calls the embeddings service (Infinity/BGE). // EmbeddingsClient calls the embeddings service (Infinity/BGE).
type EmbeddingsClient struct { type EmbeddingsClient struct {
*httpClient *httpClient
Model string Model string
} }
// NewEmbeddingsClient creates an embeddings client. // NewEmbeddingsClient creates an embeddings client.
func NewEmbeddingsClient(baseURL string, timeout time.Duration, model string) *EmbeddingsClient { func NewEmbeddingsClient(baseURL string, timeout time.Duration, model string) *EmbeddingsClient {
if model == "" { if model == "" {
model = "bge" model = "bge"
} }
return &EmbeddingsClient{httpClient: newHTTPClient(baseURL, timeout), Model: model} return &EmbeddingsClient{httpClient: newHTTPClient(baseURL, timeout), Model: model}
} }
// Embed generates embeddings for a list of texts. // Embed generates embeddings for a list of texts.
func (c *EmbeddingsClient) Embed(ctx context.Context, texts []string) ([][]float64, error) { func (c *EmbeddingsClient) Embed(ctx context.Context, texts []string) ([][]float64, error) {
body, err := c.postJSON(ctx, "/embeddings", map[string]any{ body, err := c.postJSON(ctx, "/embeddings", map[string]any{
"input": texts, "input": texts,
"model": c.Model, "model": c.Model,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
var resp struct { var resp struct {
Data []struct { Data []struct {
Embedding []float64 `json:"embedding"` Embedding []float64 `json:"embedding"`
} `json:"data"` } `json:"data"`
} }
if err := json.Unmarshal(body, &resp); err != nil { if err := json.Unmarshal(body, &resp); err != nil {
return nil, err return nil, err
} }
result := make([][]float64, len(resp.Data)) result := make([][]float64, len(resp.Data))
for i, d := range resp.Data { for i, d := range resp.Data {
result[i] = d.Embedding result[i] = d.Embedding
} }
return result, nil return result, nil
} }
// EmbedSingle generates an embedding for a single text. // EmbedSingle generates an embedding for a single text.
func (c *EmbeddingsClient) EmbedSingle(ctx context.Context, text string) ([]float64, error) { func (c *EmbeddingsClient) EmbedSingle(ctx context.Context, text string) ([]float64, error) {
results, err := c.Embed(ctx, []string{text}) results, err := c.Embed(ctx, []string{text})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(results) == 0 { if len(results) == 0 {
return nil, fmt.Errorf("empty embedding result") return nil, fmt.Errorf("empty embedding result")
} }
return results[0], nil return results[0], nil
} }
// Health checks if the embeddings service is healthy. // Health checks if the embeddings service is healthy.
func (c *EmbeddingsClient) Health(ctx context.Context) bool { func (c *EmbeddingsClient) Health(ctx context.Context) bool {
return c.healthCheck(ctx) return c.healthCheck(ctx)
} }
// ─── Reranker Client ──────────────────────────────────────────────────────── // ─── Reranker Client ────────────────────────────────────────────────────────
// RerankerClient calls the reranker service (BGE Reranker). // RerankerClient calls the reranker service (BGE Reranker).
type RerankerClient struct { type RerankerClient struct {
*httpClient *httpClient
} }
// NewRerankerClient creates a reranker client. // NewRerankerClient creates a reranker client.
func NewRerankerClient(baseURL string, timeout time.Duration) *RerankerClient { func NewRerankerClient(baseURL string, timeout time.Duration) *RerankerClient {
return &RerankerClient{httpClient: newHTTPClient(baseURL, timeout)} return &RerankerClient{httpClient: newHTTPClient(baseURL, timeout)}
} }
// RerankResult represents a reranked document. // RerankResult represents a reranked document.
type RerankResult struct { type RerankResult struct {
Index int `json:"index"` Index int `json:"index"`
Score float64 `json:"score"` Score float64 `json:"score"`
Document string `json:"document"` Document string `json:"document"`
} }
// Rerank reranks documents by relevance to the query. // Rerank reranks documents by relevance to the query.
func (c *RerankerClient) Rerank(ctx context.Context, query string, documents []string, topK int) ([]RerankResult, error) { func (c *RerankerClient) Rerank(ctx context.Context, query string, documents []string, topK int) ([]RerankResult, error) {
payload := map[string]any{ payload := map[string]any{
"query": query, "query": query,
"documents": documents, "documents": documents,
} }
if topK > 0 { if topK > 0 {
payload["top_n"] = topK payload["top_n"] = topK
} }
body, err := c.postJSON(ctx, "/rerank", payload) body, err := c.postJSON(ctx, "/rerank", payload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var resp struct { var resp struct {
Results []struct { Results []struct {
Index int `json:"index"` Index int `json:"index"`
RelevanceScore float64 `json:"relevance_score"` RelevanceScore float64 `json:"relevance_score"`
Score float64 `json:"score"` Score float64 `json:"score"`
} `json:"results"` } `json:"results"`
} }
if err := json.Unmarshal(body, &resp); err != nil { if err := json.Unmarshal(body, &resp); err != nil {
return nil, err return nil, err
} }
results := make([]RerankResult, len(resp.Results)) results := make([]RerankResult, len(resp.Results))
for i, r := range resp.Results { for i, r := range resp.Results {
score := r.RelevanceScore score := r.RelevanceScore
if score == 0 { if score == 0 {
score = r.Score score = r.Score
} }
doc := "" doc := ""
if r.Index < len(documents) { if r.Index < len(documents) {
doc = documents[r.Index] doc = documents[r.Index]
} }
results[i] = RerankResult{Index: r.Index, Score: score, Document: doc} results[i] = RerankResult{Index: r.Index, Score: score, Document: doc}
} }
return results, nil return results, nil
} }
// ─── LLM Client ───────────────────────────────────────────────────────────── // ─── LLM Client ─────────────────────────────────────────────────────────────
// LLMClient calls the vLLM-compatible LLM service. // LLMClient calls the vLLM-compatible LLM service.
type LLMClient struct { type LLMClient struct {
*httpClient *httpClient
Model string Model string
MaxTokens int MaxTokens int
Temperature float64 Temperature float64
TopP float64 TopP float64
} }
// NewLLMClient creates an LLM client. // NewLLMClient creates an LLM client.
func NewLLMClient(baseURL string, timeout time.Duration) *LLMClient { func NewLLMClient(baseURL string, timeout time.Duration) *LLMClient {
return &LLMClient{ return &LLMClient{
httpClient: newHTTPClient(baseURL, timeout), httpClient: newHTTPClient(baseURL, timeout),
Model: "default", Model: "default",
MaxTokens: 2048, MaxTokens: 2048,
Temperature: 0.7, Temperature: 0.7,
TopP: 0.9, TopP: 0.9,
} }
} }
// ChatMessage is an OpenAI-compatible message. // ChatMessage is an OpenAI-compatible message.
type ChatMessage struct { type ChatMessage struct {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
} }
// Generate sends a chat completion request and returns the response text. // Generate sends a chat completion request and returns the response text.
func (c *LLMClient) Generate(ctx context.Context, prompt string, context_ string, systemPrompt string) (string, error) { func (c *LLMClient) Generate(ctx context.Context, prompt string, context_ string, systemPrompt string) (string, error) {
messages := buildMessages(prompt, context_, systemPrompt) messages := buildMessages(prompt, context_, systemPrompt)
payload := map[string]any{ payload := map[string]any{
"model": c.Model, "model": c.Model,
"messages": messages, "messages": messages,
"max_tokens": c.MaxTokens, "max_tokens": c.MaxTokens,
"temperature": c.Temperature, "temperature": c.Temperature,
"top_p": c.TopP, "top_p": c.TopP,
} }
body, err := c.postJSON(ctx, "/v1/chat/completions", payload) body, err := c.postJSON(ctx, "/v1/chat/completions", payload)
if err != nil { if err != nil {
return "", err return "", err
} }
var resp struct { var resp struct {
Choices []struct { Choices []struct {
Message struct { Message struct {
Content string `json:"content"` Content string `json:"content"`
} `json:"message"` } `json:"message"`
} `json:"choices"` } `json:"choices"`
} }
if err := json.Unmarshal(body, &resp); err != nil { if err := json.Unmarshal(body, &resp); err != nil {
return "", err return "", err
} }
if len(resp.Choices) == 0 { if len(resp.Choices) == 0 {
return "", fmt.Errorf("no choices in LLM response") return "", fmt.Errorf("no choices in LLM response")
} }
return resp.Choices[0].Message.Content, nil return resp.Choices[0].Message.Content, nil
} }
func buildMessages(prompt, ctx, systemPrompt string) []ChatMessage { func buildMessages(prompt, ctx, systemPrompt string) []ChatMessage {
var msgs []ChatMessage var msgs []ChatMessage
if systemPrompt != "" { if systemPrompt != "" {
msgs = append(msgs, ChatMessage{Role: "system", Content: systemPrompt}) msgs = append(msgs, ChatMessage{Role: "system", Content: systemPrompt})
} else if ctx != "" { } else if ctx != "" {
msgs = append(msgs, ChatMessage{Role: "system", Content: "You are a helpful assistant. Use the provided context to answer the user's question. If the context doesn't contain relevant information, say so."}) msgs = append(msgs, ChatMessage{Role: "system", Content: "You are a helpful assistant. Use the provided context to answer the user's question. If the context doesn't contain relevant information, say so."})
} }
if ctx != "" { if ctx != "" {
msgs = append(msgs, ChatMessage{Role: "user", Content: fmt.Sprintf("Context:\n%s\n\nQuestion: %s", ctx, prompt)}) msgs = append(msgs, ChatMessage{Role: "user", Content: fmt.Sprintf("Context:\n%s\n\nQuestion: %s", ctx, prompt)})
} else { } else {
msgs = append(msgs, ChatMessage{Role: "user", Content: prompt}) msgs = append(msgs, ChatMessage{Role: "user", Content: prompt})
} }
return msgs return msgs
} }
// ─── TTS Client ───────────────────────────────────────────────────────────── // ─── TTS Client ─────────────────────────────────────────────────────────────
// TTSClient calls the TTS service (Coqui XTTS). // TTSClient calls the TTS service (Coqui XTTS).
type TTSClient struct { type TTSClient struct {
*httpClient *httpClient
Language string Language string
} }
// NewTTSClient creates a TTS client. // NewTTSClient creates a TTS client.
func NewTTSClient(baseURL string, timeout time.Duration, language string) *TTSClient { func NewTTSClient(baseURL string, timeout time.Duration, language string) *TTSClient {
if language == "" { if language == "" {
language = "en" language = "en"
} }
return &TTSClient{httpClient: newHTTPClient(baseURL, timeout), Language: language} return &TTSClient{httpClient: newHTTPClient(baseURL, timeout), Language: language}
} }
// Synthesize generates audio bytes from text. // Synthesize generates audio bytes from text.
func (c *TTSClient) Synthesize(ctx context.Context, text, language, speaker string) ([]byte, error) { func (c *TTSClient) Synthesize(ctx context.Context, text, language, speaker string) ([]byte, error) {
if language == "" { if language == "" {
language = c.Language language = c.Language
} }
params := url.Values{ params := url.Values{
"text": {text}, "text": {text},
"language_id": {language}, "language_id": {language},
} }
if speaker != "" { if speaker != "" {
params.Set("speaker_id", speaker) params.Set("speaker_id", speaker)
} }
return c.getRaw(ctx, "/api/tts", params) return c.getRaw(ctx, "/api/tts", params)
} }
// ─── STT Client ───────────────────────────────────────────────────────────── // ─── STT Client ─────────────────────────────────────────────────────────────
// STTClient calls the Whisper STT service. // STTClient calls the Whisper STT service.
type STTClient struct { type STTClient struct {
*httpClient *httpClient
Language string Language string
Task string Task string
} }
// NewSTTClient creates an STT client. // NewSTTClient creates an STT client.
func NewSTTClient(baseURL string, timeout time.Duration) *STTClient { func NewSTTClient(baseURL string, timeout time.Duration) *STTClient {
return &STTClient{httpClient: newHTTPClient(baseURL, timeout), Task: "transcribe"} return &STTClient{httpClient: newHTTPClient(baseURL, timeout), Task: "transcribe"}
} }
// TranscribeResult holds transcription output. // TranscribeResult holds transcription output.
type TranscribeResult struct { type TranscribeResult struct {
Text string `json:"text"` Text string `json:"text"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
} }
// Transcribe sends audio to Whisper and returns the transcription. // Transcribe sends audio to Whisper and returns the transcription.
func (c *STTClient) Transcribe(ctx context.Context, audio []byte, language string) (*TranscribeResult, error) { func (c *STTClient) Transcribe(ctx context.Context, audio []byte, language string) (*TranscribeResult, error) {
if language == "" { if language == "" {
language = c.Language language = c.Language
} }
fields := map[string]string{ fields := map[string]string{
"response_format": "json", "response_format": "json",
} }
if language != "" { if language != "" {
fields["language"] = language fields["language"] = language
} }
endpoint := "/v1/audio/transcriptions" endpoint := "/v1/audio/transcriptions"
if c.Task == "translate" { if c.Task == "translate" {
endpoint = "/v1/audio/translations" endpoint = "/v1/audio/translations"
} }
body, err := c.postMultipart(ctx, endpoint, "file", "audio.wav", audio, fields) body, err := c.postMultipart(ctx, endpoint, "file", "audio.wav", audio, fields)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var result TranscribeResult var result TranscribeResult
if err := json.Unmarshal(body, &result); err != nil { if err := json.Unmarshal(body, &result); err != nil {
return nil, err return nil, err
} }
return &result, nil return &result, nil
} }
// ─── Milvus Client ────────────────────────────────────────────────────────── // ─── Milvus Client ──────────────────────────────────────────────────────────
@@ -417,20 +417,20 @@ return &result, nil
// MilvusClient provides vector search via the Milvus HTTP/gRPC API. // MilvusClient provides vector search via the Milvus HTTP/gRPC API.
// For the Go port we use the Milvus Go SDK. // For the Go port we use the Milvus Go SDK.
type MilvusClient struct { type MilvusClient struct {
Host string Host string
Port int Port int
Collection string Collection string
} }
// NewMilvusClient creates a Milvus client. // NewMilvusClient creates a Milvus client.
func NewMilvusClient(host string, port int, collection string) *MilvusClient { func NewMilvusClient(host string, port int, collection string) *MilvusClient {
return &MilvusClient{Host: host, Port: port, Collection: collection} return &MilvusClient{Host: host, Port: port, Collection: collection}
} }
// SearchResult holds a single vector search hit. // SearchResult holds a single vector search hit.
type SearchResult struct { type SearchResult struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Distance float64 `json:"distance"` Distance float64 `json:"distance"`
Score float64 `json:"score"` Score float64 `json:"score"`
Fields map[string]any `json:"fields,omitempty"` Fields map[string]any `json:"fields,omitempty"`
} }