Files
e2787d3981 enhance: standardize error handling on merr + Sys/Input classification (#50221)
issue: #47420

## What this PR does

Project-wide migration of raw `fmt.Errorf` / `errors.New` in function
bodies onto
the `merr` framework, plus the Sys-vs-Input error classification and the
machinery it drives (retriability, fine-grained metrics, segcore
unification),
plus the convention docs and a linter that keeps it from regressing.

Scope: storage, proxy, coordinators (root/data/query), query node, data
node,
`pkg/util` & `internal/util`, expression parser, message queue,
streaming, and
misc packages. Bare raw-error usages went from ~3000 to a ~340 allowlist
(package-level sentinels / build-tag / test sites).

---

## How to review this PR

It is large but the vast majority is mechanical. Changes fall into three
tiers;
spend review budget on Part 2 and Part 3.

### Part 1 — Mechanical standardization (low risk, verify by rule)

Each converted call follows one of a small fixed set of rules. To
review, check
that each site obeys the matching rule rather than reading every line:

| Pattern | Rule |
|---|---|
| `fmt.Errorf("...")` originating a new error | →
`merr.WrapErrXxxMsg("...")` with a code matching the failure's meaning |
| Adding context to an existing typed error | → `merr.Wrap(err, "...")`
/ `merr.Wrapf(...)` — **preserves** the inner code (never `WrapErr*Err`,
which overwrites it) |
| Errors inside the streaming subsystem | → `status.New*` factories
(StreamingError), **not** merr — this is the component-internal dialect
(see `docs/dev/error_handling_guide.md`) |
| Low-level / control-flow signal caught by `errors.Is` | → kept as a
package-level `errors.New` sentinel (lowercase, same-package) |

Conventions are documented in `docs/dev/error_handling_guide.md`
(how-to) and
`docs/dev/error_sentinel_convention.md` (rules + audit). A
`gocritic`/`ruleguard`
rule (`rawmerrerror`, in `rules.go`) enforces "no raw `return
errors.New/fmt.Errorf`"
under `make verifiers`.

### Part 2 — Behavior changes (review these closely)

These are the sites where the wire contract or runtime behavior changes,
not just
the source text. Listed by category; representative locations given,
full set in
the diff.

**A. gRPC wire-code shifts: `UnexpectedError(1)/Code 65535` → typed
code.**
Where a handler previously returned a raw error (collapsed to
`Code=65535` on the
wire), it now returns a typed merr, so the client sees a real code. The
most
common shift is to `IllegalArgument(5)/Code 1100` (ParameterInvalid).
Touch
points include datanode task handlers (CreateTask/Query/Drop), proxy
Upsert,
querynode GetMetrics, datacoord CreateIndex, httpserver query-response
builder,
and typeutil schema validation. One code refinement: an index-param
validation
moved `1100` → `1101` (ParameterMissing). **Client/SDK assertions and
any code
that switched on `Code=65535` for these paths must be re-checked** (the
go_client
e2e assertions were already aligned in this PR).

**B. Prometheus `status` label contract change (externally visible).**
The proxy metric's coarse `fail` / `rejected` values are split into
`fail_input` / `fail_system` and `rejected_user` / `rejected_system` (in
`requestutil.ParseMetricLabel`; auth/privilege rejections count as
`rejected_user`), so dashboards can attribute a failure to caller vs
operator.
**Dashboards/alerts querying `status="fail"` must migrate to
`status=~"fail_.*"`, and `status="rejected"` to
`status=~"rejected_.*"`.** The
in-repo Grafana dashboard is already migrated; external dashboards built
on the
old values silently go empty after upgrade. This is the one change that
requires an ops-side migration.

**C. Retriability semantics.**
- C1: `merr.Status(err)` now forces `Retriable=false` when the error is
an
`InputError` — a malformed request can never succeed on blind retry, so
clients
never get the self-contradictory "your input is wrong but you may
retry".
- C2: `retry.Do` short-circuits an `InputError` (non-retriable) — **but
only when
  the caller did not pass a `RetryErr` predicate**. The check is an
`if c.isRetryErr != nil { ... } else if InputError { ... }` *mutually
exclusive*
branch (`pkg/util/retry/retry.go`): an explicit `RetryErr` takes
precedence and
bypasses the InputError abort. `retry.Handle` deliberately does **not**
apply
the InputError abort (its callers signal abort via `shouldRetry=false`).
Four
flusher startup callsites that must retry through transient "not ready"
errors
  were given explicit `RetryErr` escape hatches.

**D. segcore (C++→Go) error classification.**
A single shared Go-side table (`pkg/util/merr/segcore.go`) maps each
segcore code
to a merr sentinel + InputError/signal category, replacing scattered
hand-written
`if errorCode == ...` switches in the cgo wrappers. **Wire `Code` values
change
for every segcore pass-through error, not just the remapped ones.**
Named
sentinels remap (C++ `2003` → merr `2001`, `2033` → `2002`,
Folly/Knowhere codes
likewise); **all remaining pass-through codes (`2004`–`2043`, previously
surfaced to clients as raw C++ enum values) now serialize as `2000`**
(`ErrSegcore`), with the original C++ code preserved in the `Reason`
text
(`segcoreCode=...`); unknown/future codes collapse to `2000` as well
(pinned by
the `wire_code_projection` test). Transient segcore classes (object
storage /
file IO / OOM / mmap / FieldNotLoaded — 11 codes) now report
`Retriable=true`.
**Any client switching on raw segcore codes in the `2004`–`2043` range
must be
re-checked**; the in-Reason code remains available for diagnostics.
Signal
codes (PretendFinished / FollyCancel) are recognized centrally.
`errors.Is`-based
control flow on these (e.g. scheduler skip/retry) is preserved.

**E. InputError classification (25 sentinels + dynamic marks).**
25 sentinels in `errors.go` carry `WithErrorType(InputError)` (the
Collection /
ResourceGroup / Database families, `ErrIndexDuplicate`,
`ErrParameterInvalid`,
`ErrPrivilegeNotAuthenticated`, `ErrImportFailed`, `ErrQueryPlan`, ...),
plus dynamic
marks for the 8 segcore input codes (ExprInvalid, DimNotMatch,
MetricTypeInvalid, FieldIDInvalid, ...) and
`WrapErrAsInputError`. The widest blast radius is `ErrParameterInvalid`
(1100):
~2335 `WrapErrParameterInvalid*` callsites now classify as input /
non-retriable. Because of C1/C2 this changes retriability for
any path that returns these. **The audit to confirm no transient path
was
mis-marked is the single most important review item** (see Part 3). One
reverse
correction: storage field-stats parsing moved from `ErrParameterInvalid`
(input)
to `ErrDataIntegrity` — a corrupted stored stat is data corruption, not
user
input.

### Part 3 — Known risks & traps (called out proactively)

1. **`merr.Wrap` vs `WrapErr*Err` (code-masking).** `WrapErr*Err` builds
a
`wrappedMilvusError{sentinel: ErrServiceInternal}` whose `code()`
returns the
*outer* sentinel — it overwrites the inner typed code and hides the
`errors.Is`
chain. This is intentional (use it to *deliberately* downgrade), but it
was a
recurring conversion defect; the rule "add context with `merr.Wrap`,
downgrade
with `WrapErr*Err`" is enforced by convention and reviewed across the
diff.
2. **InputError × `retry.Do` blast radius.** Marking a sentinel
`InputError` makes
any `retry.Do(...)` without a `RetryErr` predicate stop retrying it.
Reviewers
should sanity-check that no transient use of the 19 newly-marked
sentinels
(especially `ErrParameterInvalid`) sits inside a retry loop that needed
to keep
   spinning. The known flusher cases were handled (see C2).
3. **The ~340 raw-error allowlist.** What remains as bare `errors.New`
is, by
design: package-level sentinels (caught by `errors.Is`), `//go:build
test`
sites, and out-of-band trees (`cmd/`, `tests/`, codegen, walimpls). The
linter
only bans the *direct-return* form; assignment-then-return escapes and
the full
no-exceptions ban are deferred to an AST-based linter (Tier 2,
documented).
4. **segcore C++ second step deferred.** This PR unifies classification
on the Go
side; splitting the dual-semantic C++ codes at the source is a
follow-up.

---

## Validation

- `make verifiers`: Go side clean (gofmt + static-check across modules,
including
  the new `rawmerrerror` rule with a 0-hit baseline repo-wide).
- `make test-go`: passing; the one real regression introduced (a
datanode
`invalid_task_type` assertion shifting `1` → `5` from a ParameterInvalid
  conversion) was fixed in-tree.
- go_client e2e CreateIndex assertions aligned to the new merr messages.

---------

Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 15:04:51 -07:00

587 lines
17 KiB
Go

package function
/*
#cgo pkg-config: milvus_core
#include <stdint.h>
#include <stdlib.h>
#include "segcore/minhash_c.h"
#include "segcore/tokenizer_c.h"
*/
import "C"
import (
"encoding/binary"
"strconv"
"strings"
"sync"
"unsafe"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/util/analyzer"
"github.com/milvus-io/milvus/internal/util/analyzer/canalyzer"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
)
// MinHashFunctionRunner
// Input: string (text)
// Output: []byte (binary vector - MinHash signature vector)
const (
// outter parameters
NumHashesKey = "num_hashes"
ShingleSizeKey = "shingle_size"
HashFuncKey = "hash_function"
TokenLevelKey = "token_level" // "char" for character-level n-grams, "word" for word-level (default)
SeedKey = "seed"
// internal parameters
defaultShingleSize = 3
defaultSeed = 1234
)
// HashFunction type
type HashFunction int
const (
// todo: support more hash functions
HashFuncSHA1 HashFunction = iota
HashFuncXXHash64
)
type MinHashFunctionRunner struct {
mu sync.RWMutex
closed bool
tokenizer analyzer.Analyzer // word-level tokenizer
funSchema *schemapb.FunctionSchema
inputField *schemapb.FieldSchema
outputField *schemapb.FieldSchema
// MinHash specific parameters
numHashes int // MinHash signature vector dimension
shingleSize int // N-gram, N size
hashFunc HashFunction // Hash function to use
useCharToken bool // true: character-level n-grams, false: word-level tokens + shingles
// Universal hash family parameters: h(x) = ((a * x + b) mod p) mod m
// Each permutation has its own (a, b) pair
permA []uint64 // 'a' (must be odd for full period)
permB []uint64 // 'b'
}
func NewMinHashFunctionRunner(
collSchema *schemapb.CollectionSchema,
funSchema *schemapb.FunctionSchema,
) (FunctionRunner, error) {
if len(funSchema.GetOutputFieldIds()) != 1 {
return nil, merr.WrapErrParameterInvalidMsg("minhash function should only have one output field, but now %d", len(funSchema.GetOutputFieldIds()))
}
if len(funSchema.GetInputFieldIds()) != 1 {
return nil, merr.WrapErrParameterInvalidMsg("minhash function should only have one input field, but now %d", len(funSchema.GetInputFieldIds()))
}
var inputField, outputField *schemapb.FieldSchema
for _, field := range collSchema.GetFields() {
if field.GetFieldID() == funSchema.GetOutputFieldIds()[0] {
outputField = field
}
if field.GetFieldID() == funSchema.GetInputFieldIds()[0] {
inputField = field
}
}
if outputField == nil {
return nil, merr.WrapErrParameterInvalidMsg("no output field")
}
if inputField == nil {
return nil, merr.WrapErrParameterInvalidMsg("no input field")
}
params := getAnalyzerParams(inputField)
tokenizer, err := analyzer.NewAnalyzer(params, "")
if err != nil {
return nil, err
}
numHashes := 0
shingleSize := defaultShingleSize
hashFunc := HashFuncXXHash64 // Default to xxHash for better performance
useCharToken := false // Default to word-level Token
seed := defaultSeed
var permA, permB []uint64
for _, param := range funSchema.GetParams() {
switch strings.ToLower(param.GetKey()) {
case NumHashesKey:
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("param num_hashes:%s is not a number", param.GetValue())
}
if val <= 0 {
return nil, merr.WrapErrParameterInvalidMsg("param num_hashes:%d must be positive", val)
}
numHashes = int(val)
case ShingleSizeKey:
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("param shingle_size:%s is not a number", param.GetValue())
}
if val <= 0 {
return nil, merr.WrapErrParameterInvalidMsg("param shingle_size:%d must be positive", val)
}
shingleSize = int(val)
case HashFuncKey:
switch strings.ToLower(param.GetValue()) {
case "xxhash", "xxhash64":
hashFunc = HashFuncXXHash64
case "sha1":
hashFunc = HashFuncSHA1
default:
return nil, merr.WrapErrParameterInvalidMsg("unknown hash function: %s", param.GetValue())
}
case TokenLevelKey:
switch strings.ToLower(param.GetValue()) {
case "char", "character":
useCharToken = true
case "word":
useCharToken = false
default:
return nil, merr.WrapErrParameterInvalidMsg("unknown token_level: %s (expected 'char' or 'word')", param.GetValue())
}
case SeedKey:
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("param seed:%s is not a number", param.GetValue())
}
seed = int(val)
}
}
if numHashes <= 0 {
// auto generate numHashes from output field dim
var outputDim int64 = -1
for _, param := range outputField.GetTypeParams() {
if param.GetKey() == "dim" {
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err == nil {
outputDim = val
break
}
}
}
if outputDim <= 0 || outputDim%32 != 0 {
return nil, merr.WrapErrParameterInvalidMsg("minhash function output field '%s' dim not found or invalid(dim > 0, dim %% 32 == 0)", outputField.GetName())
}
numHashes = int(outputDim / 32)
funSchema.Params = append(funSchema.Params, &commonpb.KeyValuePair{
Key: NumHashesKey,
Value: strconv.Itoa(numHashes),
})
}
// Initialize permutations
permA, permB = initializePermutations(numHashes, int64(seed))
runner := &MinHashFunctionRunner{
tokenizer: tokenizer,
funSchema: funSchema,
inputField: inputField,
outputField: outputField,
numHashes: numHashes,
shingleSize: shingleSize,
hashFunc: hashFunc,
useCharToken: useCharToken,
permA: permA,
permB: permB,
}
return runner, nil
}
func ValidateMinHashFunction(collSchema *schemapb.CollectionSchema, funSchema *schemapb.FunctionSchema) error {
var inputField, outputField *schemapb.FieldSchema
// check input field count
if len(funSchema.GetInputFieldNames()) != 1 {
return merr.WrapErrParameterInvalidMsg("minhash function should only have one input field, but now %d", len(funSchema.GetInputFieldNames()))
}
if len(funSchema.GetOutputFieldNames()) != 1 {
return merr.WrapErrParameterInvalidMsg("minhash function should only have one output field, but now %d", len(funSchema.GetOutputFieldNames()))
}
// Find fields by name (since FieldIDs may not be assigned yet during validation)
inputFieldName := funSchema.GetInputFieldNames()[0]
outputFieldName := funSchema.GetOutputFieldNames()[0]
for _, field := range collSchema.GetFields() {
if field.GetName() == inputFieldName {
inputField = field
}
if field.GetName() == outputFieldName {
outputField = field
}
}
if inputField == nil {
return merr.WrapErrParameterInvalidMsg("minhash function input field '%s' not found", inputFieldName)
}
if outputField == nil {
return merr.WrapErrParameterInvalidMsg("minhash function output field '%s' not found", outputFieldName)
}
if inputField.GetDataType() != schemapb.DataType_VarChar && inputField.GetDataType() != schemapb.DataType_String {
return merr.WrapErrParameterInvalidMsg("minhash function input field '%s' is not string type, is %s",
inputFieldName, inputField.GetDataType())
}
// check function params
numHashes := int(-1)
for _, param := range funSchema.GetParams() {
switch strings.ToLower(param.GetKey()) {
case NumHashesKey:
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("param num_hashes:%s is not a number", param.GetValue())
}
numHashes = int(val)
if numHashes <= 0 {
return merr.WrapErrParameterInvalidMsg("param num_hashes:%d must be positive", numHashes)
}
case ShingleSizeKey:
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("param shingle_size:%s is not a number", param.GetValue())
}
if val <= 0 {
return merr.WrapErrParameterInvalidMsg("param shingle_size:%d must be positive", val)
}
case HashFuncKey:
switch strings.ToLower(param.GetValue()) {
case "xxhash", "xxhash64", "sha1":
// valid hash function
default:
return merr.WrapErrParameterInvalidMsg("unknown hash function: %s (expected 'xxhash64' or 'sha1')", param.GetValue())
}
case TokenLevelKey:
switch strings.ToLower(param.GetValue()) {
case "char", "character", "word":
// valid token level
default:
return merr.WrapErrParameterInvalidMsg("unknown token_level: %s (expected 'char' or 'word')", param.GetValue())
}
case SeedKey:
_, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("param seed:%s is not a number", param.GetValue())
}
}
}
// check numHashes with output field
var outputDim int64 = -1
if outputField.GetDataType() != schemapb.DataType_BinaryVector {
return merr.WrapErrParameterInvalidMsg("minhash function output field '%s' is not binary vector type", outputFieldName)
}
for _, param := range outputField.GetTypeParams() {
if param.GetKey() == "dim" {
val, err := strconv.ParseInt(param.GetValue(), 10, 64)
if err == nil {
outputDim = val
break
}
}
}
if numHashes > 0 {
expectedDim := int64(numHashes * 32) // binary vector, each hash is 4 bytes (32 bits), but stored as 8 bits in binary vector
if outputDim != expectedDim {
return merr.WrapErrParameterInvalidMsg("minhash function output field '%s' dim %d does not match expected dim %d (numHashes %d * one minhash signature size of 32bit)", outputFieldName, outputDim, expectedDim, numHashes)
}
} else {
if outputDim%32 != 0 {
return merr.WrapErrParameterInvalidMsg("minhash function output field '%s' dim %d is not multiple of 32 (one minhash signature size)", outputFieldName, outputDim)
}
}
// else no numHashes specified, skip output field validation
return nil
}
func (m *MinHashFunctionRunner) run(data []string, dst [][]byte) error {
// Clone the appropriate tokenizer based on mode
var wordTokenizer analyzer.Analyzer
var err error
if !m.useCharToken {
// Word-level mode: use word tokenizer
wordTokenizer, err = m.tokenizer.Clone()
if err != nil {
return err
}
defer wordTokenizer.Destroy()
}
// Phase 1 & 2: Generate hashes and compute MinHash signatures
var allSignatures [][]uint32
// Everything happens in C++ to eliminate ALL CGO overhead:
// - Tokenization/character processing in C++
// - Shingle generation in C++
// - Base hash computation in C++
// - MinHash signature computation with rotation-based SIMD in C++
var tokenizerPtr unsafe.Pointer
if !m.useCharToken {
// Word-level: get C tokenizer pointer
tokenizerPtr = getTokenizerPtr(wordTokenizer)
}
// Char-level: tokenizerPtr is nil, C++ will process characters directly
allSignatures = m.batchComputeMinHashFromTexts(data, tokenizerPtr)
// Phase 3: Batch convert to binary vectors
batchSignatureToBinaryVector(allSignatures, dst)
return nil
}
func (m *MinHashFunctionRunner) BatchRun(inputs ...any) ([]any, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.closed {
return nil, merr.WrapErrServiceInternalMsg("MinHash function closed")
}
if len(inputs) > 1 {
return nil, merr.WrapErrParameterInvalidMsg("MinHash function received more than one input column")
}
text, ok := inputs[0].([]string)
if !ok {
return nil, merr.WrapErrParameterInvalidMsg("MinHash function input not string list")
}
rowNum := len(text)
signatures := make([][]byte, rowNum)
concurrency := 8
if rowNum < concurrency {
concurrency = rowNum
}
wg := sync.WaitGroup{}
errCh := make(chan error, concurrency)
for i, j := 0, 0; i < concurrency && j < rowNum; i++ {
start := j
end := start + rowNum/concurrency
if i < rowNum%concurrency {
end += 1
}
wg.Add(1)
go func() {
defer wg.Done()
err := m.run(text[start:end], signatures[start:end])
if err != nil {
errCh <- err
}
}()
j = end
}
wg.Wait()
close(errCh)
for err := range errCh {
if err != nil {
return nil, err
}
}
return []any{buildBinaryVectorFieldData(signatures)}, nil
}
func (v *MinHashFunctionRunner) GetSchema() *schemapb.FunctionSchema {
return v.funSchema
}
func (m *MinHashFunctionRunner) GetOutputFields() []*schemapb.FieldSchema {
return []*schemapb.FieldSchema{m.outputField}
}
func (v *MinHashFunctionRunner) GetInputFields() []*schemapb.FieldSchema {
return []*schemapb.FieldSchema{v.inputField}
}
func (m *MinHashFunctionRunner) Close() {
m.mu.Lock()
defer m.mu.Unlock()
if !m.closed {
if m.tokenizer != nil {
m.tokenizer.Destroy()
}
m.closed = true
}
}
func (m *MinHashFunctionRunner) batchComputeMinHashFromTexts(texts []string, tokenizerPtr unsafe.Pointer) [][]uint32 {
if len(texts) == 0 {
return nil
}
// Prepare text data - calculate total bytes needed
totalBytes := 0
for _, text := range texts {
totalBytes += len(text)
}
// Allocate buffer for all texts
cBuffer := C.malloc(C.size_t(totalBytes))
defer C.free(cBuffer)
// Prepare pointer and length arrays
cTexts := make([]unsafe.Pointer, len(texts))
textLengths := make([]int32, len(texts))
// Copy texts into buffer
cBufferSlice := unsafe.Slice((*byte)(cBuffer), totalBytes)
bufferOffset := 0
for i, text := range texts {
textLen := len(text)
if textLen > 0 {
copy(cBufferSlice[bufferOffset:bufferOffset+textLen], text)
cTexts[i] = unsafe.Pointer(&cBufferSlice[bufferOffset])
} else {
cTexts[i] = nil
}
textLengths[i] = int32(textLen)
bufferOffset += textLen
}
// Allocate output buffer (flattened)
flatSignatures := make([]uint32, len(texts)*m.numHashes)
// Call C++ end-to-end implementation
C.ComputeMinHashFromTexts(
(**C.char)(unsafe.Pointer(&cTexts[0])),
(*C.int32_t)(unsafe.Pointer(&textLengths[0])),
C.int32_t(len(texts)),
tokenizerPtr,
C.int32_t(m.shingleSize),
(*C.uint64_t)(unsafe.Pointer(&m.permA[0])),
(*C.uint64_t)(unsafe.Pointer(&m.permB[0])),
C.int32_t(m.hashFunc),
C.int32_t(m.numHashes),
(*C.uint32_t)(unsafe.Pointer(&flatSignatures[0])),
)
// Convert flattened output to [][]uint32 using slicing (zero-copy view)
signatures := make([][]uint32, len(texts))
for i := 0; i < len(texts); i++ {
start := i * m.numHashes
end := start + m.numHashes
signatures[i] = flatSignatures[start:end]
}
return signatures
}
// helper function to get analyzer params
// getTokenizerPtr extracts the underlying C tokenizer pointer from an Analyzer
func getTokenizerPtr(a analyzer.Analyzer) unsafe.Pointer {
if cAnalyzer, ok := a.(*canalyzer.CAnalyzer); ok {
// Use reflection or provide a public method in CAnalyzer to get the pointer
// For now, we'll need to add a public method to CAnalyzer
return cAnalyzer.GetCPtr()
}
return nil
}
func initializePermutations(numHashes int, seed int64) ([]uint64, []uint64) {
if numHashes <= 0 {
return nil, nil
}
permA := make([]uint64, numHashes)
permB := make([]uint64, numHashes)
C.InitPermutations(
C.int32_t(numHashes),
C.uint64_t(seed),
(*C.uint64_t)(unsafe.Pointer(&permA[0])),
(*C.uint64_t)(unsafe.Pointer(&permB[0])),
)
return permA, permB
}
func signatureToBinaryVector(signature []uint32) []byte {
byteLength := len(signature) * 4
result := make([]byte, byteLength)
i := 0
for ; i+4 <= len(signature); i += 4 {
offset := i * 4
binary.LittleEndian.PutUint32(result[offset:offset+4], signature[i])
binary.LittleEndian.PutUint32(result[offset+4:offset+8], signature[i+1])
binary.LittleEndian.PutUint32(result[offset+8:offset+12], signature[i+2])
binary.LittleEndian.PutUint32(result[offset+12:offset+16], signature[i+3])
}
for ; i < len(signature); i++ {
hash := signature[i]
offset := i * 4
binary.LittleEndian.PutUint32(result[offset:offset+4], hash)
}
return result
}
// batchSignatureToBinaryVector converts multiple signatures to binary vectors in batch
// This improves cache locality and reduces function call overhead
func batchSignatureToBinaryVector(signatures [][]uint32, dst [][]byte) {
if len(signatures) == 0 {
return
}
signatureByteLen := len(signatures[0]) * 4
for batchIdx := 0; batchIdx < len(signatures); batchIdx++ {
signature := signatures[batchIdx]
result := make([]byte, signatureByteLen)
i := 0
for ; i+4 <= len(signature); i += 4 {
offset := i * 4
binary.LittleEndian.PutUint32(result[offset:], signature[i])
binary.LittleEndian.PutUint32(result[offset+4:], signature[i+1])
binary.LittleEndian.PutUint32(result[offset+8:], signature[i+2])
binary.LittleEndian.PutUint32(result[offset+12:], signature[i+3])
}
// Handle remaining elements
for ; i < len(signature); i++ {
offset := i * 4
binary.LittleEndian.PutUint32(result[offset:], signature[i])
}
dst[batchIdx] = result
}
}
func buildBinaryVectorFieldData(signatures [][]byte) *schemapb.FieldData {
var dim int64
var flatData []byte
if len(signatures) > 0 {
dim = int64(len(signatures[0]) * 8)
flatData = make([]byte, 0, len(signatures)*len(signatures[0]))
for _, sig := range signatures {
flatData = append(flatData, sig...)
}
}
return &schemapb.FieldData{
Type: schemapb.DataType_BinaryVector,
Field: &schemapb.FieldData_Vectors{
Vectors: &schemapb.VectorField{
Dim: dim,
Data: &schemapb.VectorField_BinaryVector{
BinaryVector: flatData,
},
},
},
}
}