Inverted files and sparse data
Inverted files (InvertedFiles submodule)
Inverted file index data structures and context for sparse vectors, MIPS, and set search.
SimilaritySearch.InvertedFiles.AbstractInvertedFile — Type
abstract type AbstractInvertedFile <: AbstractSearchIndex endAbstract inverted file; the concrete data structure is InvertedFile.
SimilaritySearch.InvertedFiles.InvertedFile — Type
struct InvertedFile{DistType<:PreMetric, AdjType<:AbstractAdjList, DbType<:AbstractDatabase} <: AbstractInvertedFileA general-purpose inverted index: a sparse matrix-like representation mapping component dimensions (or set elements/tokens) to identifiers (AdjType's element type is UInt32, plain token/set membership; other concrete adjacency element types, e.g. a compressed encoding, can be added by extending getcontainer, internal_push!, and sort_postinglist!). It always keeps the original indexed object in db.
Fields
dist: distance function used at search time (e.g.Dist.Sets.Jaccard(),Dist.NormCosine()).adj: posting lists (non-zero id-elements, in rows).sizes: number of non-zero values in each element (non-zero values in columns); resized/populated only up tolen[].db: the original indexed objects, one per identifier; always populated bypush_item!/append_items!, but may hold more objects than have actually been indexed – seelen.len: number of objects already indexed (postings built); may be less thanlength(database(idx))ifdbwas grown directly without a followingindex!call to catch up.
For a handful of distances (the set metrics in Dist.Sets, see InvertedFiles.has_exact_fastpath) the score computed while merging posting lists is already exact, at O(1) cost. For any other distance (including Dist.NormCosine), every merge candidate is instead scored by evaluating dist directly against the objects stored in db, so results for that path are exact too — the number of such evaluations (hence cost) is controlled by the t-threshold parameter of search; raise t above the default 1 to bound the number of real evaluations per query.
SimilaritySearch.InvertedFiles.DictInvertedFile — Type
const DictInvertedFile{DistType, KeyType, DbType} = InvertedFile{DistType, AdjDict{KeyType, UInt32}, DbType}A dictionary-backed inverted file mapping posting list keys of type KeyType (e.g., String, Vector{UInt8}, NTuple, Int) to document identifiers (UInt32). Empty or non-existent posting lists are never stored in memory or disk, enabling use over arbitrary or massive key spaces.
Constructors
DictInvertedFile(::Type{KeyType}, dist::PreMetric=Dist.Sets.Jaccard(); db::AbstractDatabase=VectorDatabase(Any[]), hint_size::Integer=0)DictInvertedFile(dist::PreMetric=Dist.Sets.Jaccard(); KeyType::Type=Any, db::AbstractDatabase=VectorDatabase(Any[]), hint_size::Integer=0)
SimilaritySearch.InvertedFiles.search_invfile — Function
search_invfile(idx::InvertedFile, ctx::InvertedFileContext, q, Q, res::AbstractKnnQueue, t)
Find candidates for solving query Q using idx. It calls callback on each candidate (objID, dist)
Arguments
idx: inverted indexq: the query object, only used for distances without an exact fast path (seeInvertedFiles.has_exact_fastpath)Q: the set of involved posting lists, seeselect_posting_listst: threshold (t=1 union, t > 1 solves the t-threshold problem); for distances without an exact fast path,talso bounds how many realevaluatecalls happen per query — raise it to reduce cost.
SimilaritySearch.InvertedFiles.select_posting_lists — Function
select_posting_lists(idx::AbstractInvertedFile, ctx::InvertedFileContext, q)Fetches and prepares the involved posting lists to solve q
SimilaritySearch.InvertedFiles.has_exact_fastpath — Function
has_exact_fastpath(dist::PreMetric)::BoolWhether the score computed while merging posting lists (via set_distance_evaluate) is already the exact dist value. When false, search_invfile instead evaluates dist directly against the objects stored in the index's db for every merge candidate — see FallbackInvFileOutput in invfilesearch.jl; raise t above the default 1 to bound how many such evaluations happen per query.
SimilaritySearch.InvertedFiles.identiterator — Function
identiterator(obj)Iterator over the plain ids/keys in obj, for callers that only need to know which ids/keys are present (e.g. InvertedFile building/re-sorting/searching its posting lists, which never need a weight: the handful of distances with an exact fast path score from intersection size and set sizes alone, and any other distance is evaluated directly against the full objects kept in db – see InvertedFile). Dense Vectors are not accepted directly – convert to a SparseVector first (e.g. via SparseArrays.sparse) so the reduction to non-zero components is explicit in the caller's code.
identiterator(dist::PreMetric, obj)Distance-aware id-only iterator for obj. Defaults to the distance-agnostic identiterator(obj) dispatch tree above; overload this for a specific (DistType, ObjType) pair when the same native object type must generate different candidate ids depending on which distance the enclosing index is built for (e.g. a shingle-based candidate encoding for a sequence distance).
SimilaritySearch.InvertedFiles.sort_postinglist! — Function
sort_postinglist!(adj::AbstractAdjList, N)Sorts a single posting list N (as returned by neighbors(adj, tokenID)) back into the order the merge/search algorithms rely on: ascending by id, for plain token adjacency (UInt32). Override for a different concrete adjacency element type (e.g. a compressed encoding).
SimilaritySearch.InvertedFiles.InvertedFileContext — Type
InvertedFileContext(; verbose, reporters, observers, maxbatches, parallel_block, scheduler, ...)The context an inverted file is searched and built with. It holds what a query needs besides the index itself: the logging channels, the batching parameters, the per-batch cost counters, and the scratch buffers that a posting-list merge reuses instead of allocating.
It plays the same role for an inverted file that SearchGraphContext plays for a graph. Use getcontext to obtain one that matches a given index, since the buffers depend on the key type the index uses.
SimilaritySearch.InvertedFiles.getcontext — Function
getcontext(invfile::AbstractInvertedFile; kwargs...) -> InvertedFileContextA context whose buffers match invfile. The scratch buffers a merge reuses are typed by the index's key type, so building the context from the index is safer than building it by hand. The keyword arguments are those of InvertedFileContext.
SimilaritySearch.InvertedFiles.set_distance_evaluate — Function
set_distance_evaluate(dist::PreMetric, intersection::Integer, size1::Integer, size2::Integer)Computes a score for a candidate found while merging posting lists, given the intersection size of the matching posting lists and the total number of non-zero entries of each of the two compared elements (size1, size2). Only defined for the handful of distances with an exact closed form (see has_exact_fastpath) — the resulting value is exact, computed purely from these three integers, with no need to touch the original objects. For any other dist, search_invfile does not call this function at all — it evaluates dist directly against the stored objects for every merge candidate instead (see FallbackInvFileOutput in invfilesearch.jl); use t > 1 to bound how many such evaluations happen per query.
Posting list intersections (Intersections submodule)
Algorithms for set and posting list intersections.
SimilaritySearch.Intersections.bk! — Function
bk!(output, L, P, findpos::Function=doublingsearch) -> int. sizeComputes the intersection of a list of posting lists using the Barbay and Kenyon algorithm.
See umerge! if you need to change how matches are captured (onmatch! function).
SimilaritySearch.Intersections.bkt! — Function
bkt!(output, L, [P,], [findpos::Function=doublingsearch]; t::Int=length(L)) -> num. matchesBarybay & Kenyon t-thresholds
See umerge! if you need to change how matches are captured (onmatch! function).
SimilaritySearch.Intersections.umerge! — Function
umerge!(output, L, P=ones(Int32, length(L_)); t::Int=1) -> num. matchesMerges posting lists in L and saves the union in output. The merge result is stored into output array. You can customize how to do this specializing the onmatch!(output, L, P, t::Int) function.
Arguments:
L: The array of posting lists, the array can be destroyed in the process.P: The array of current positions in posting lists, i.e., initial state as an array of ones of size $|L|$.t: Computes t-thresholds, i.e., t from 1 (union) to |L| (intersection) of posting lists inLusingfindposstoring the result set inoutput.
About the callback function
output, L and P are the arguments same than the input, while t is the actual number of lists having the match. Note 1: you should access L[i][P[i]] to get the entry of the ith list, i.e., $1 \leq t \leq |P|$. Note 2: L and P as container lists will be also modified, the contained lists remain untouched.
SimilaritySearch.Intersections.xmerge! — Function
xmerge!(output, L, P=ones(Int32, length(L_)); t::Int=1) -> num. matchesSolves t-threshold set operation using other algorithms choosing among them by given t
Arguments:
output: vector like to store the t-threshold setL: the list of posting lists to be merged. The posting lists are left untouched but the container is modified.P: indices of the current merging-state (idem toL)t: the threshold, i.e.,t=1(union) ...t=|L|performs intersection)
Simple wrapper around other specific operations depending on t value
See umerge! if you need to modify the output behaviour.
SimilaritySearch.Intersections.svs — Function
svs(postinglists, intersect2=baezayates!) -> outputComputes the intersection of the ordered lists in postinglists using a small vs small strategy. Accepts an intersection algorithm of two sets.
This method does not give explicit support for onmatch2!
SimilaritySearch.Intersections.binarysearch — Function
binarysearch(A, x, sp=1, ep=length(A))
Finds the insertion position of x in A in the range sp:ep
SimilaritySearch.Intersections.doublingsearch — Function
doublingsearch(A, x, sp=1, ep=length(A))
Finds the insertion position of x in A, starting at sp
SimilaritySearch.Intersections.doublingsearchrev — Function
doublingsearchrev(A, x, sp=1, ep=length(A))
Finds the insertion position of x in A, starting at the end
SimilaritySearch.Intersections.seqsearch — Function
seqsearch(A, x, sp=1, ep=length(A))
Sequential search, i.e., it starts from sp to ep
SimilaritySearch.Intersections.seqsearchrev — Function
seqsearchrev(A, x, sp=1, ep=length(A))
Reverse sequential search, i.e., it starts from ep to sp
SimilaritySearch.Intersections.imerge2! — Function
imerge2!(output, A, B) -> num. matchesComputes the intersection of A and B (sorted arrays), using a merge-like algorithm, and stores it in output or calls onmatch(A, i, B, j) on every match. See bk! for how change the behaviour of matches.
Spherical embedding for MIPS (Special.Spherical submodule)
Turns Maximum Inner Product Search into ordinary nearest-neighbor search (Neyshabur & Srebro's asymmetric spherical embedding), for dense and sparse vectors alike.
SimilaritySearch.Special.Spherical — Module
SphericalImplements the spherical embedding of Neyshabur & Srebro, "On Symmetric and Asymmetric LSHs for Inner Product Search" (2015): a dataset-dependent transform that turns Maximum Inner Product Search (MIPS) into ordinary nearest-neighbor search under a standard metric (e.g. squared Euclidean or NormCosine).
The scheme is asymmetric: the dataset and the queries are mapped with two different functions, transform/transform! (data-side, P) and transform_query/transform_query! (query-side, Q):
\[P(x) = \left[\frac{x}{M}, \sqrt{1 - \left\|\frac{x}{M}\right\|^2}\right] \qquad Q(q) = \left[\frac{q}{\|q\|}, 0\right]\]
where $M$ is the maximum norm over the fitted dataset. Both P(x) and Q(q) land exactly on the unit sphere, and for any fixed query, ranking data points by increasing ||P(x) - Q(q)|| (or decreasing dot product) recovers exactly the ranking by decreasing inner product $x \cdot q$ – M and \|q\| are constants w.r.t. x, so they do not affect the ordering. See SphericalEmbedding for how M is fitted and stored, and its docstring for the caveat that matters when the underlying database keeps growing.
Both dense (AbstractVector/AbstractMatrix/MatrixDatabase) and sparse (Special.Sparse.SparseVecView/SparseDatabase, and plain SparseArrays.SparseVector/ SparseMatrixCSC) representations are supported.
SimilaritySearch.Special.Spherical.SphericalEmbedding — Type
SphericalEmbedding(X::AbstractMatrix; pad::Bool=true, padmultiple::Int=8, maxnorm=nothing)
SphericalEmbedding(db::MatrixDatabase; pad::Bool=true, padmultiple::Int=8, maxnorm=nothing)
SphericalEmbedding(X::SparseMatrixCSC; maxnorm=nothing)
SphericalEmbedding(db::Special.Sparse.SparseDatabase; maxnorm=nothing)Fits a spherical embedding (Neyshabur & Srebro) over a dataset, storing the metadata needed by transform/transform!/transform_query/ transform_query!: the dataset's maximum norm maxnorm (M), its input dimension indim, and (dense only) an optional number of extra zero-padding coordinates pad. The embedded (output) dimension is indim + pad + 1 (the +1 is the residual-norm coordinate appended by transform); see outdim.
Since maxnorm is computed once, from the dataset given here, this struct is exactly the "fitted state" that must be saved and reused: apply the SAME SphericalEmbedding to the dataset (via transform) and to every later query (via transform_query) – never re-fit a new one per query.
maxnorm is a property of the dataset at fit time. If the database keeps growing and a later vector's norm exceeds maxnorm, this SphericalEmbedding is stale: see the warning on transform! for what happens and what to do about it (refit a new SphericalEmbedding, recomputing maxnorm over the enlarged dataset).
Arguments
X/db: the dataset to fit against; dense forms accept anAbstractMatrix(columns are objects) or aMatrixDatabase; sparse forms accept aSparseMatrixCSC(columns are objects) or aSpecial.Sparse.SparseDatabase.
Keyword Arguments
pad: dense only. Whentrue(the default), pads the embedded dimension up to the next multiple ofpadmultiplewith extra zero coordinates – zero-valued coordinates do not change any dot product/norm, so this is purely a memory-layout knob (e.g. for SIMD-friendly alignment), never a correctness one. Sparse inputs do not support padding (there is nothing to align; padding a sparse vector would just mean storing explicit zeros).padmultiple: dense only, the multiple to padindim + 1up to whenpad=true(default8).maxnorm: an optional precomputed maximum norm to use instead of scanningX/db(e.g. to reuse a previously-fitted value, or to deliberately fit a looser bound that tolerates some future growth without going stale).
Examples
julia> using SimilaritySearch, SimilaritySearch.Special.Spherical
julia> X = rand(Float32, 8, 1000);
julia> se = SphericalEmbedding(X);
julia> outdim(se) # 8 + pad + 1
16
julia> P = transform(se, X); # embed the dataset
julia> q = rand(Float32, 8);
julia> Qq = transform_query(se, q); # embed a query the SAME way
julia> length(Qq) == outdim(se)
trueSimilaritySearch.Special.Spherical.outdim — Function
outdim(se::SphericalEmbedding) -> IntOutput (embedded) dimension produced by transform/transform_query: indim(se) + pad + 1 (the +1 is the appended coordinate – the residual norm for transform, always 0 for transform_query).
SimilaritySearch.Special.Spherical.indim — Function
indim(se::SphericalEmbedding) -> IntInput dimension expected by transform/transform_query: the dimension of the original vectors, not counting any padding or the appended residual coordinate.
SimilaritySearch.Special.Spherical.transform — Function
transform(se::SphericalEmbedding, x::AbstractVector) -> Vector{Float32}Out-of-place version of transform!: returns a freshly allocated Vector{Float32} of length outdim(se).
transform(se::SphericalEmbedding, X::AbstractMatrix; minbatch::Int=4) -> Matrix{Float32}Embeds every column (object) of X using transform, returning a new Matrix{Float32} with outdim(se) rows and the same number of columns as X.
transform(se::SphericalEmbedding, db::MatrixDatabase; minbatch::Int=4) -> MatrixDatabaseEmbeds every object of db using transform, returning a new MatrixDatabase wrapping a freshly allocated Matrix{Float32}.
transform(se::SphericalEmbedding, x::Special.Sparse.SparseVecView) -> SparseVecViewSparse-vector version of transform: scales the stored nonzero entries of x by 1/se.maxnorm and appends one explicit (outdim(se), residual) entry, reusing Special.Sparse's existing distance machinery unchanged (the appended index is always the largest, so the result stays sorted). se must have been fitted without padding (se.pad == 0, the default for sparse inputs).
transform(se::SphericalEmbedding, x::SparseArrays.SparseVector) -> SparseArrays.SparseVectorVersion of transform for a plain SparseArrays.SparseVector (as opposed to Special.Sparse.SparseVecView); same semantics.
transform(se::SphericalEmbedding, X::SparseMatrixCSC) -> SparseMatrixCSCEmbeds every column of the sparse matrix X (see transform), returning a new SparseMatrixCSC with outdim(se) rows and the same number of columns as X.
transform(se::SphericalEmbedding, db::Special.Sparse.SparseDatabase) -> SparseDatabaseEmbeds every object of db using transform, returning a new SparseDatabase.
SimilaritySearch.Special.Spherical.transform! — Function
transform!(se::SphericalEmbedding, out::AbstractVector, x::AbstractVector) -> outIn-place data-side spherical embedding (P(x), see Spherical): scales x by 1/se.maxnorm, fills any padding coordinates with zero, and appends the residual-norm coordinate sqrt(1 - ||x/maxnorm||^2), so that out lands exactly on the unit sphere. out must have length outdim(se).
If norm(x) > se.maxnorm – e.g. x is a new vector inserted into a database that kept growing after se was fitted – the residual term's argument goes negative; it is clamped to 0 here rather than throwing a DomainError, but the ranking guarantee described in Spherical no longer holds for x (and for every comparison against it) once that happens. When the dataset can keep growing, either refit a new SphericalEmbedding (recomputing maxnorm over the enlarged dataset) or fit the original one with a deliberately loose maxnorm= bound that tolerates the growth you expect.
transform!(se::SphericalEmbedding, O::AbstractMatrix, X::AbstractMatrix; minbatch::Int=4) -> OIn-place version of transform(se, X): embeds every column of X (see transform!) into the corresponding column of O, which must have outdim(se) rows and the same number of columns as X. Columns are embedded in parallel using @BATCHES.
SimilaritySearch.Special.Spherical.transform_query — Function
transform_query(se::SphericalEmbedding, q::AbstractVector) -> Vector{Float32}Out-of-place version of transform_query!.
transform_query(se::SphericalEmbedding, Q::AbstractMatrix; minbatch::Int=4) -> Matrix{Float32}Embeds every column (query) of Q using transform_query, returning a new Matrix{Float32} with outdim(se) rows and the same number of columns as Q.
transform_query(se::SphericalEmbedding, q::Special.Sparse.SparseVecView) -> SparseVecView
transform_query(se::SphericalEmbedding, q::SparseArrays.SparseVector) -> SparseArrays.SparseVectorSparse-vector version of transform_query. Unlike transform's sparse methods, no extra entry is appended – the query's appended coordinate is always exactly 0, and sparse formats already represent unlisted entries as 0 implicitly.
SimilaritySearch.Special.Spherical.transform_query! — Function
transform_query!(se::SphericalEmbedding, out::AbstractVector, q::AbstractVector) -> outIn-place query-side spherical embedding (Q(q), see Spherical): unlike transform!, q is scaled by its OWN norm (not se.maxnorm), and every padding coordinate together with the final appended coordinate is set to 0 (there is no residual term to compute on the query side – it is always exactly 0, by construction). out must have length outdim(se). A zero q maps to an all-zero out.
Must be applied with the SAME se used to transform the dataset being searched – se only contributes indim/outdim bookkeeping here (maxnorm is not used at all), so, unlike transform!, this function itself never goes stale as the dataset grows; only re-fitting se (which changes outdim) would require re-embedding queries.
transform_query!(se::SphericalEmbedding, O::AbstractMatrix, Q::AbstractMatrix; minbatch::Int=4) -> OIn-place version of transform_query(se, Q).
transform_query!(se::SphericalEmbedding, q::Special.Sparse.SparseVecView) -> SparseVecView
transform_query!(se::SphericalEmbedding, q::SparseArrays.SparseVector) -> SparseArrays.SparseVectorIn-place version of transform_query for sparse vectors. This method modifies the input vector's values in-place (avoiding allocations for the values), but it returns a new vector/view object because the output dimensionality is outdim(se) (typically dim + 1).
Sparse vector support (Special.Sparse submodule)
A sparse matrix view tailored for distance evaluations, replacing Base's SparseVector with an explicit dimension-tracking read-only wrapper SparseVecView.
SimilaritySearch.Special.Sparse.sparsedot — Function
sparsedot(a, b; small_threshold::Int=30, ratio_threshold::Float32=3.0f0)Adaptive dot product between two SparseVectors or SparseVecViews:
- both sides have fewer than
small_thresholdstored entries, or their sizes are withinratio_thresholdof each other: a plain linear merge. - otherwise (one side much larger than the other): a Hwang-Lin/galloping merge.
SimilaritySearch.Special.Sparse.centroid — Function
centroid(cluster::AbstractVector{<:SparseVectorLike})Centroid (normalized sum) of a cluster of SparseVectors. See sum(::AbstractVector{<:SparseVectorLike}) for the algorithm.