@zilliz/milvus2-sdk-node
Version:
Official Node.js SDK for [Milvus](https://github.com/milvus-io/milvus) vector database. Provides gRPC and HTTP clients for vector similarity search, metadata filtering, and full collection/index/user management.
157 lines (156 loc) • 7.63 kB
TypeScript
/**
* Client-side Split-Block Bloom Filter (SBBF) construction for the
* `membership_match(field, {blob}, type=bloom)` filter expression.
*
* Building the filter on the client and shipping the compact blob lets large membership
* sets pass the proxy gRPC receive limit, which a raw value list would exceed: 10M int64
* values are roughly 90 MB as an `in [...]` list but about 14 MB as a bloom filter. The
* proxy embeds the blob verbatim after validating the envelope and never rebuilds the
* filter, so these bytes must match what the other SDKs produce exactly.
*
* The bit layout is bit-identical to Arrow C++'s `parquet::BlockSplitBloomFilter`, and thus
* to the parquet-format BloomFilter.md spec, wrapped in the Milvus MBF1 envelope. See
* `docs/design-docs/design_docs/20260707-bloom-filter-expression.md` in the milvus repo.
*/
/** The 4-byte MBF1 envelope magic. */
export declare const BLOOM_FILTER_MAGIC = "MBF1";
/** The MBF1 envelope version implemented here. */
export declare const BLOOM_FILTER_VERSION = 1;
/** Identifies the parquet SBBF + XXH64 algorithm. */
export declare const BLOOM_FILTER_ALGO_PARQUET_SBBF_XXH64 = 1;
/** Size in bytes of the MBF1 envelope header. */
export declare const BLOOM_FILTER_HEADER_SIZE = 32;
/** Size of one SBBF block (parquet-format spec). */
export declare const BLOOM_FILTER_BYTES_PER_BLOCK = 32;
/** Marks a filter that recorded int64 values (8-byte little-endian hash domain). */
export declare const BLOOM_FILTER_DOMAIN_INT64 = 1;
/** Marks a filter that recorded string values (raw UTF-8 hash domain). */
export declare const BLOOM_FILTER_DOMAIN_UTF8 = 2;
/**
* Minimum / maximum filter body size, mirroring Arrow's
* BlockSplitBloomFilter::kMinimumBloomFilterBytes / kMaximumBloomFilterBytes.
*/
export declare const BLOOM_FILTER_MIN_BYTES = 32;
export declare const BLOOM_FILTER_MAX_BYTES: number;
/** Lowest / highest accepted false-positive rate. */
export declare const BLOOM_FILTER_MIN_FPR = 0.0001;
export declare const BLOOM_FILTER_MAX_FPR = 0.05;
/**
* Recommended false-positive rate when a caller has no specific target. Sizing follows the
* Arrow formula, so a body holds roughly 0.72 members per byte at this rate. Bodies are
* powers of two, so a member count just past a tier boundary doubles the blob; raising fpr
* is usually the cheaper fix.
*/
export declare const BLOOM_FILTER_DEFAULT_FPR = 0.005;
/**
* XXH64 with seed 0 over raw bytes — the hash the SBBF spec mandates.
*
* BigInt rather than a 32-bit-pair implementation: JavaScript numbers cannot represent a
* 64-bit hash, and every intermediate here (multiply, rotate) genuinely needs the full
* width. Build cost is dominated by this, so the int64 domain gets a specialised path below.
*/
export declare const xxh64: (data: Uint8Array) => bigint;
/**
* XXH64 over an int64's 8-byte little-endian encoding, without materialising those bytes.
*
* Specialisation of {@link xxh64} for the one input length the int64 domain ever produces:
* the 32-byte stripe loop and the 4-byte / 1-byte tails are unreachable, leaving a single
* 8-byte lane. `value & MASK64` is exactly the little-endian two's-complement encoding read
* back as a u64, so the pack/unpack round trip drops out.
*/
export declare const xxh64Int64: (value: bigint) => bigint;
/**
* XXH64 (seed 0) over raw bytes via the 32-bit-pair path, returned as a bigint.
*
* This is what {@link BloomFilterBuilder.addString} actually runs; {@link xxh64} is the
* BigInt reference kept to check it against. Exported so that check can be a test.
*/
export declare const xxh64Pairs: (data: Uint8Array) => bigint;
/**
* Returns the exact byte length {@link buildBloomFilter} would produce for `n` members at
* the given false-positive rate, without hashing anything.
*
* Use it to check a planned filter against the proxy limits before building it: the body
* must fit `proxy.maxBloomFilterSize` (64 MiB by default) and the whole request must fit
* `proxy.grpc.serverMaxRecvSize` (128 MiB by default).
*/
export declare const estimateBloomFilterSize: (n: number, fpr: number) => number;
/**
* Incrementally constructs an SBBF and serializes it into an MBF1 envelope.
*
* Unlike {@link buildBloomFilter} a builder accepts both value domains, producing a filter
* that matches integer and string members alike — useful for a JSON path that legitimately
* holds both.
*/
export declare class BloomFilterBuilder {
private readonly buf;
private readonly view;
private readonly numBlocks;
/** 32 - log2(numBlocks), and numBlocks - 1: together they reduce a hash to a block index. */
private readonly blockShift;
private readonly blockMask;
private readonly nDeclared;
private readonly fpr;
private domains;
/**
* @param n expected number of distinct members; it only drives sizing, so inserting more
* is allowed and simply raises the effective false-positive rate
* @param fpr false-positive rate, in [BLOOM_FILTER_MIN_FPR, BLOOM_FILTER_MAX_FPR]
*/
constructor(n: number, fpr?: number);
/** Inserts an integer value, hashed as its 8-byte little-endian encoding. */
addInt64(value: number | bigint): this;
/**
* Inserts a string value, hashed as its raw UTF-8 bytes.
*
* `Buffer.from` rather than `TextEncoder`: over 10M members the encode step alone is 0.41s
* via Buffer against 1.48s for a hoisted TextEncoder and 1.81s for a per-call one. Buffer
* is a Uint8Array subclass, and `xxh64` reads through the view's byteOffset, so the pooled
* backing store Buffer hands out is handled correctly.
*/
addString(value: string): this;
/** Returns the value domains inserted so far. Zero means nothing was inserted. */
getDomains(): number;
/** Returns the number of 32-byte blocks in the filter body. */
getNumBlocks(): number;
/**
* Stamps the MBF1 header onto the filter and returns the envelope.
*
* The returned array is the builder's own buffer, not a copy, so a filter costs one
* allocation of its final size rather than a body plus an equal-sized serialization
* buffer. Treat it as read-only, and copy it if you keep inserting afterwards.
*/
build(): Uint8Array;
/**
* Sets this hash's eight bits directly in the final MBF1 buffer.
*
* The block index needs the full 64-bit product, so it stays in BigInt; everything after
* is 32-bit, where `Math.imul` gives exactly the wrapping multiply the spec wants and
* `>>> 27` the logical shift.
*/
private addHash;
}
/**
* Builds an MBF1-wrapped Split-Block Bloom Filter over a membership set.
*
* Members must be homogeneous: either all integers (`number` or `bigint`) or all strings.
* An empty list produces a valid blob that records no domain and therefore matches nothing.
*
* Build the blob from the same value domain as the target field: integer fields hash int64,
* VARCHAR fields hash UTF-8. The envelope records which domains were inserted, so a
* wrong-domain blob is rejected by the proxy with a parameter error rather than silently
* matching nothing.
*
* ```ts
* const blob = buildBloomFilter(userIds);
* await client.query({
* collection_name: 'docs',
* filter: 'membership_match(user_id, {bf}, type=bloom)',
* exprValues: { bf: blob },
* });
* ```
*
* @param members the membership set
* @param fpr false-positive rate, in [BLOOM_FILTER_MIN_FPR, BLOOM_FILTER_MAX_FPR]
*/
export declare const buildBloomFilter: (members: Array<number | bigint | string>, fpr?: number) => Uint8Array;