diff --git a/Makefile b/Makefile index b73271860a..70c8b1986a 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,10 @@ include/sourmash.h: src/core/src/lib.rs \ src/core/src/ffi/minhash.rs \ src/core/src/ffi/signature.rs \ src/core/src/ffi/nodegraph.rs \ + src/core/src/ffi/index/mod.rs \ + src/core/src/ffi/index/linear.rs \ + src/core/src/index/mod.rs \ + src/core/src/index/linear.rs \ src/core/src/errors.rs cd src/core && \ RUSTUP_TOOLCHAIN=nightly cbindgen -c cbindgen.toml . -o ../../$@ diff --git a/include/sourmash.h b/include/sourmash.h index 7f88fcc203..ccd88f3c1f 100644 --- a/include/sourmash.h +++ b/include/sourmash.h @@ -16,6 +16,13 @@ enum HashFunctions { }; typedef uint32_t HashFunctions; +enum SearchType { + SEARCH_TYPE_JACCARD = 1, + SEARCH_TYPE_CONTAINMENT = 2, + SEARCH_TYPE_MAX_CONTAINMENT = 3, +}; +typedef uint32_t SearchType; + enum SourmashErrorCode { SOURMASH_ERROR_CODE_NO_ERROR = 0, SOURMASH_ERROR_CODE_PANIC = 1, @@ -50,8 +57,14 @@ typedef struct SourmashHyperLogLog SourmashHyperLogLog; typedef struct SourmashKmerMinHash SourmashKmerMinHash; +typedef struct SourmashLinearIndex SourmashLinearIndex; + typedef struct SourmashNodegraph SourmashNodegraph; +typedef struct SourmashSearchFn SourmashSearchFn; + +typedef struct SourmashSearchResult SourmashSearchResult; + typedef struct SourmashSignature SourmashSignature; /** @@ -248,6 +261,26 @@ void kmerminhash_slice_free(uint64_t *ptr, uintptr_t insize); bool kmerminhash_track_abundance(const SourmashKmerMinHash *ptr); +const SourmashSearchResult *const *linearindex_find(const SourmashLinearIndex *ptr, + const SourmashSearchFn *search_fn_ptr, + const SourmashSignature *sig_ptr, + uintptr_t *size); + +void linearindex_free(SourmashLinearIndex *ptr); + +void linearindex_insert_many(SourmashLinearIndex *ptr, + const SourmashSignature *const *search_sigs_ptr, + uintptr_t insigs); + +uintptr_t linearindex_len(const SourmashLinearIndex *ptr); + +SourmashLinearIndex *linearindex_new(void); + +SourmashLinearIndex *linearindex_new_with_sigs(const SourmashSignature *const *search_sigs_ptr, + uintptr_t insigs); + +SourmashSignature **linearindex_signatures(const SourmashLinearIndex *ptr, uintptr_t *size); + void nodegraph_buffer_free(uint8_t *ptr, uintptr_t insize); bool nodegraph_count(SourmashNodegraph *ptr, uint64_t h); @@ -292,6 +325,18 @@ SourmashNodegraph *nodegraph_with_tables(uintptr_t ksize, uintptr_t starting_size, uintptr_t n_tables); +void searchfn_free(SourmashSearchFn *ptr); + +SourmashSearchFn *searchfn_new(SearchType search_type, double threshold, bool best_only); + +SourmashStr searchresult_filename(const SourmashSearchResult *ptr); + +void searchresult_free(SourmashSearchResult *ptr); + +double searchresult_score(const SourmashSearchResult *ptr); + +SourmashSignature *searchresult_signature(const SourmashSearchResult *ptr); + void signature_add_protein(SourmashSignature *ptr, const char *sequence); void signature_add_sequence(SourmashSignature *ptr, const char *sequence, bool force); diff --git a/nix/rust.nix b/nix/rust.nix index 5883fd3d52..8aaae712e3 100644 --- a/nix/rust.nix +++ b/nix/rust.nix @@ -3,7 +3,7 @@ let pkgs = import sources.nixpkgs { overlays = [ (import sources.rust-overlay) ]; }; - rustVersion = pkgs.rust-bin.stable.latest.rust.override { + rustVersion = pkgs.rust-bin.nightly.latest.rust.override { #extensions = [ "rust-src" ]; #targets = [ "x86_64-unknown-linux-musl" ]; targets = [ "wasm32-wasi" "wasm32-unknown-unknown" ]; diff --git a/shell.nix b/shell.nix index d63f2ea99b..d8dae33d4a 100644 --- a/shell.nix +++ b/shell.nix @@ -13,12 +13,14 @@ in (python38.withPackages(ps: with ps; [ virtualenv tox setuptools ])) (python39.withPackages(ps: with ps; [ virtualenv setuptools ])) (python37.withPackages(ps: with ps; [ virtualenv setuptools ])) + rust-cbindgen py-spy heaptrack cargo-watch cargo-limit wasmtime wasm-pack + gdb ]; shellHook = '' diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 3de8c37734..2c9997604d 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -43,6 +43,7 @@ serde_json = "1.0.53" primal-check = "0.3.1" thiserror = "1.0" typed-builder = "0.9.0" +atomic_float = "0.1.0" [target.'cfg(all(target_arch = "wasm32", target_vendor="unknown"))'.dependencies.wasm-bindgen] version = "0.2.62" diff --git a/src/core/src/ffi/index/linear.rs b/src/core/src/ffi/index/linear.rs new file mode 100644 index 0000000000..9568240e6e --- /dev/null +++ b/src/core/src/ffi/index/linear.rs @@ -0,0 +1,116 @@ +use std::slice; + +use crate::index::linear::LinearIndex; +use crate::index::{Index, SigStore}; +use crate::signature::Signature; + +use crate::ffi::index::SourmashSearchResult; +use crate::ffi::search::SourmashSearchFn; +use crate::ffi::signature::SourmashSignature; +use crate::ffi::utils::ForeignObject; + +pub struct SourmashLinearIndex; + +impl ForeignObject for SourmashLinearIndex { + type RustObject = LinearIndex; +} + +#[no_mangle] +pub unsafe extern "C" fn linearindex_new() -> *mut SourmashLinearIndex { + SourmashLinearIndex::from_rust(LinearIndex::builder().build()) +} + +ffi_fn! { +unsafe fn linearindex_new_with_sigs( + search_sigs_ptr: *const *const SourmashSignature, + insigs: usize, +) -> Result<*mut SourmashLinearIndex> { + let search_sigs: Vec> = { + assert!(!search_sigs_ptr.is_null()); + slice::from_raw_parts(search_sigs_ptr, insigs) + .iter() + .map(|sig| SourmashSignature::as_rust(*sig).clone().into()) + .collect() + }; + + let linear_index = LinearIndex::builder().datasets(search_sigs).build(); + + Ok(SourmashLinearIndex::from_rust(linear_index)) +} +} + +ffi_fn! { +unsafe fn linearindex_insert_many( + ptr: *mut SourmashLinearIndex, + search_sigs_ptr: *const *const SourmashSignature, + insigs: usize, +) -> Result<()> { + let index = SourmashLinearIndex::as_rust_mut(ptr); + + slice::from_raw_parts(search_sigs_ptr, insigs) + .iter() + .try_for_each(|sig| { + let s = SourmashSignature::as_rust(*sig).clone(); + index.insert(s) + }) +} +} + +#[no_mangle] +pub unsafe extern "C" fn linearindex_free(ptr: *mut SourmashLinearIndex) { + SourmashLinearIndex::drop(ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn linearindex_len(ptr: *const SourmashLinearIndex) -> usize { + let index = SourmashLinearIndex::as_rust(ptr); + index.len() +} + +ffi_fn! { +unsafe fn linearindex_signatures(ptr: *const SourmashLinearIndex, + size: *mut usize) -> Result<*mut *mut SourmashSignature> { + let index = SourmashLinearIndex::as_rust(ptr); + + let sigs = index.signatures(); + + // FIXME: use the ForeignObject trait, maybe define new method there... + let ptr_sigs: Vec<*mut SourmashSignature> = sigs.into_iter().map(|x| { + Box::into_raw(Box::new(x)) as *mut SourmashSignature + }).collect(); + + let b = ptr_sigs.into_boxed_slice(); + *size = b.len(); + + Ok(Box::into_raw(b) as *mut *mut SourmashSignature) +} +} + +ffi_fn! { +unsafe fn linearindex_find( + ptr: *const SourmashLinearIndex, + search_fn_ptr: *const SourmashSearchFn, + sig_ptr: *const SourmashSignature, + size: *mut usize, +) -> Result<*const *const SourmashSearchResult> { + let linearindex = SourmashLinearIndex::as_rust(ptr); + let search_fn = SourmashSearchFn::as_rust(search_fn_ptr); + let query = SourmashSignature::as_rust(sig_ptr); + + let results: Vec<(f64, Signature, String)> = linearindex + .find_new(search_fn, query)? + .into_iter() + .collect(); + + // FIXME: use the ForeignObject trait, maybe define new method there... + let ptr_sigs: Vec<*const SourmashSearchResult> = results + .into_iter() + .map(|x| Box::into_raw(Box::new(x)) as *const SourmashSearchResult) + .collect(); + + let b = ptr_sigs.into_boxed_slice(); + *size = b.len(); + + Ok(Box::into_raw(b) as *const *const SourmashSearchResult) +} +} diff --git a/src/core/src/ffi/index/mod.rs b/src/core/src/ffi/index/mod.rs new file mode 100644 index 0000000000..bbe038e37d --- /dev/null +++ b/src/core/src/ffi/index/mod.rs @@ -0,0 +1,37 @@ +pub mod linear; + +use crate::signature::Signature; + +use crate::ffi::signature::SourmashSignature; +use crate::ffi::utils::{ForeignObject, SourmashStr}; + +pub struct SourmashSearchResult; + +impl ForeignObject for SourmashSearchResult { + type RustObject = (f64, Signature, String); +} + +#[no_mangle] +pub unsafe extern "C" fn searchresult_free(ptr: *mut SourmashSearchResult) { + SourmashSearchResult::drop(ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn searchresult_score(ptr: *const SourmashSearchResult) -> f64 { + let result = SourmashSearchResult::as_rust(ptr); + result.0 +} + +#[no_mangle] +pub unsafe extern "C" fn searchresult_filename(ptr: *const SourmashSearchResult) -> SourmashStr { + let result = SourmashSearchResult::as_rust(ptr); + (result.2).clone().into() +} + +#[no_mangle] +pub unsafe extern "C" fn searchresult_signature( + ptr: *const SourmashSearchResult, +) -> *mut SourmashSignature { + let result = SourmashSearchResult::as_rust(ptr); + SourmashSignature::from_rust((result.1).clone()) +} diff --git a/src/core/src/ffi/mod.rs b/src/core/src/ffi/mod.rs index bfd9b46bd7..326591d499 100644 --- a/src/core/src/ffi/mod.rs +++ b/src/core/src/ffi/mod.rs @@ -8,8 +8,10 @@ pub mod utils; pub mod cmd; pub mod hyperloglog; +pub mod index; pub mod minhash; pub mod nodegraph; +pub mod search; pub mod signature; use std::ffi::CStr; diff --git a/src/core/src/ffi/search.rs b/src/core/src/ffi/search.rs new file mode 100644 index 0000000000..0547bca9f9 --- /dev/null +++ b/src/core/src/ffi/search.rs @@ -0,0 +1,25 @@ +use crate::index::{JaccardSearch, SearchType}; + +use crate::ffi::utils::ForeignObject; + +pub struct SourmashSearchFn; + +impl ForeignObject for SourmashSearchFn { + type RustObject = JaccardSearch; +} + +#[no_mangle] +pub unsafe extern "C" fn searchfn_free(ptr: *mut SourmashSearchFn) { + SourmashSearchFn::drop(ptr); +} + +#[no_mangle] +pub unsafe extern "C" fn searchfn_new( + search_type: SearchType, + threshold: f64, + best_only: bool, +) -> *mut SourmashSearchFn { + let mut func = JaccardSearch::with_threshold(search_type, threshold); + func.set_best_only(best_only); + SourmashSearchFn::from_rust(func) +} diff --git a/src/core/src/ffi/utils.rs b/src/core/src/ffi/utils.rs index 69baac7b88..b4c1947e22 100644 --- a/src/core/src/ffi/utils.rs +++ b/src/core/src/ffi/utils.rs @@ -314,3 +314,7 @@ pub unsafe extern "C" fn sourmash_str_free(s: *mut SourmashStr) { (*s).free() } } + +impl ForeignObject for SourmashStr { + type RustObject = SourmashStr; +} diff --git a/src/core/src/index/linear.rs b/src/core/src/index/linear.rs index 009ebbaadc..0e9a18f64b 100644 --- a/src/core/src/index/linear.rs +++ b/src/core/src/index/linear.rs @@ -1,3 +1,4 @@ +use std::convert::TryInto; use std::fs::File; use std::io::{BufReader, Read}; use std::path::Path; @@ -7,9 +8,13 @@ use std::rc::Rc; use serde::{Deserialize, Serialize}; use typed_builder::TypedBuilder; -use crate::index::storage::{FSStorage, ReadData, Storage, StorageInfo, ToWriter}; -use crate::index::{Comparable, DatasetInfo, Index, SigStore}; +use crate::index::{Comparable, DatasetInfo, Index, JaccardSearch, SigStore}; +use crate::signature::{Signature, SigsTrait}; use crate::Error; +use crate::{ + index::storage::{FSStorage, ReadData, Storage, StorageInfo, ToWriter}, + sketch::Sketch, +}; #[derive(TypedBuilder)] pub struct LinearIndex { @@ -30,7 +35,7 @@ struct LinearInfo { impl<'a, L> Index<'a> for LinearIndex where L: Clone + Comparable + 'a, - SigStore: From, + SigStore: From + ReadData, { type Item = L; //type SignatureIterator = std::slice::Iter<'a, Self::Item>; @@ -58,15 +63,13 @@ where fn signatures(&self) -> Vec { self.datasets .iter() - .map(|x| x.data.get().unwrap().clone()) + .map(|x| (*x).data().unwrap()) + .cloned() .collect() } fn signature_refs(&self) -> Vec<&Self::Item> { - self.datasets - .iter() - .map(|x| x.data.get().unwrap()) - .collect() + self.datasets.iter().map(|x| (*x).data().unwrap()).collect() } /* @@ -182,4 +185,59 @@ where pub fn storage(&self) -> Option> { self.storage.clone() } + + pub fn len(&self) -> usize { + self.datasets.len() + } +} + +impl LinearIndex { + pub fn find_new( + &self, + search_fn: &JaccardSearch, + query: &Signature, + ) -> Result, Error> { + search_fn.check_is_compatible(&query)?; + + let query_mh; + if let Sketch::MinHash(mh) = &query.signatures[0] { + query_mh = mh; + } else { + unimplemented!() + } + + // TODO: prepare_subject and prepare_query + let location: String = "TODO".into(); + + Ok(self + .datasets + .iter() + .filter_map(|subj| { + let subj_sig = subj.data().unwrap(); + let subj_mh; + if let Sketch::MinHash(mh) = &subj_sig.signatures[0] { + subj_mh = mh; + } else { + unimplemented!() + } + + let (shared_size, total_size) = dbg!(query_mh.intersection_size(&subj_mh).unwrap()); + let query_size = query_mh.size(); + let subj_size = subj_mh.size(); + + let score: f64 = search_fn.score( + query_size.try_into().unwrap(), + shared_size, + subj_size.try_into().unwrap(), + total_size, + ); + + if search_fn.passes(score) && search_fn.collect(score, subj) { + Some((score, subj_sig.clone(), location.clone())) + } else { + None + } + }) + .collect()) + } } diff --git a/src/core/src/index/mod.rs b/src/core/src/index/mod.rs index 507020fe3c..a26392c683 100644 --- a/src/core/src/index/mod.rs +++ b/src/core/src/index/mod.rs @@ -14,7 +14,9 @@ pub mod search; use std::ops::Deref; use std::path::Path; use std::rc::Rc; +use std::sync::atomic::Ordering; +use atomic_float::AtomicF64; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use typed_builder::TypedBuilder; @@ -330,3 +332,100 @@ impl From for SigStore { } } } + +#[repr(u32)] +pub enum SearchType { + Jaccard = 1, + Containment = 2, + MaxContainment = 3, +} + +pub struct JaccardSearch { + search_type: SearchType, + threshold: AtomicF64, + require_scaled: bool, + best_only: bool, +} + +impl JaccardSearch { + pub fn new(search_type: SearchType) -> Self { + let require_scaled = match search_type { + SearchType::Containment | SearchType::MaxContainment => true, + SearchType::Jaccard => false, + }; + + JaccardSearch { + search_type, + threshold: AtomicF64::new(0.0), + require_scaled, + best_only: false, + } + } + + pub fn with_threshold(search_type: SearchType, threshold: f64) -> Self { + let s = Self::new(search_type); + s.set_threshold(threshold); + s + } + + pub fn set_best_only(&mut self, best_only: bool) { + self.best_only = best_only; + } + + pub fn set_threshold(&self, threshold: f64) { + self.threshold + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |_| Some(threshold)) + .unwrap(); + } + + pub fn check_is_compatible(&self, sig: &Signature) -> Result<(), Error> { + // TODO: implement properly + Ok(()) + } + + pub fn score( + &self, + query_size: u64, + shared_size: u64, + subject_size: u64, + total_size: u64, + ) -> f64 { + let shared_size = shared_size as f64; + match self.search_type { + SearchType::Jaccard => shared_size / total_size as f64, + SearchType::Containment => { + if query_size == 0 { + 0.0 + } else { + shared_size / query_size as f64 + } + } + SearchType::MaxContainment => { + let min_denom = query_size.min(subject_size); + if min_denom == 0 { + 0.0 + } else { + shared_size / min_denom as f64 + } + } + } + } + + /// Return True if this match should be collected. + pub fn collect(&self, score: f64, subj: &Signature) -> bool { + if self.best_only { + self.threshold.fetch_max(score, Ordering::Relaxed); + } + true + } + + /// Return true if this score meets or exceeds the threshold. + /// + /// Note: this can be used whenever a score or estimate is available + /// (e.g. internal nodes on an SBT). `collect(...)`, below, decides + /// whether a particular signature should be collected, and/or can + /// update the threshold (used for BestOnly behavior). + pub fn passes(&self, score: f64) -> bool { + score > 0. && score >= self.threshold.load(Ordering::SeqCst) + } +} diff --git a/src/sourmash/index.py b/src/sourmash/index.py index 55c3a2f8c4..583d5bd4e6 100644 --- a/src/sourmash/index.py +++ b/src/sourmash/index.py @@ -1,13 +1,16 @@ "An Abstract Base Class for collections of signatures." import os +import weakref import sourmash from abc import abstractmethod, ABC from collections import namedtuple, Counter import zipfile import copy +from .utils import RustObject, rustcall, decode_str, encode_str from .search import make_jaccard_search_query, make_gather_query +from ._lowlevel import ffi, lib # generic return tuple for Index.search and Index.gather IndexSearchResult = namedtuple('Result', 'score, signature, location') @@ -322,29 +325,90 @@ def select_signature(ss, ksize=None, moltype=None, scaled=0, num=0, return True -class LinearIndex(Index): +class LinearIndex(Index, RustObject): "An Index for a collection of signatures. Can load from a .sig file." + + __dealloc_func__ = lib.linearindex_free + def __init__(self, _signatures=None, filename=None): - self._signatures = [] - if _signatures: - self._signatures = list(_signatures) self.filename = filename + self._objptr = ffi.NULL + + self.__signatures = [] + if not _signatures: + # delay initialization for when we have signatures + return + + self.__signatures = _signatures + self._init_inner() + + def _init_inner(self): + if self._objptr != ffi.NULL and not self.__signatures: + # Already initialized, nothing new to add + return + + if (not self.__signatures and self._objptr == ffi.NULL): + # no signatures provided, initializing empty LinearIndex + self._objptr = lib.linearindex_new() + return + + attached_refs = weakref.WeakKeyDictionary() + + collected = [] + # pass SourmashSignature pointers to LinearIndex. + for sig in self.__signatures: + rv = sig._get_objptr() + attached_refs[rv] = (rv, sig) + collected.append(rv) + search_sigs_ptr = ffi.new("SourmashSignature*[]", collected) + self.__signatures = [] + + if self._objptr != ffi.NULL: + # new signatures to add, insert to already initialized LinearIndex + self._methodcall( + lib.linearindex_insert_many, + search_sigs_ptr, + len(search_sigs_ptr) + ) + else: + # Rust object was not initialized yet, so let's create it with the + # new sigs + self._objptr = rustcall( + lib.linearindex_new_with_sigs, + search_sigs_ptr, + len(search_sigs_ptr), + ) @property def location(self): return self.filename def signatures(self): - return iter(self._signatures) + from sourmash import SourmashSignature + + self._init_inner() + + size = ffi.new("uintptr_t *") + sigs_ptr = self._methodcall(lib.linearindex_signatures, size) + size = size[0] + + sigs = [] + for i in range(size): + sig = SourmashSignature._from_objptr(sigs_ptr[i]) + sigs.append(sig) + + for sig in sigs: + yield sig def __bool__(self): - return bool(self._signatures) + return bool(len(self)) def __len__(self): - return len(self._signatures) + self._init_inner() + return self._methodcall(lib.linearindex_len) def insert(self, node): - self._signatures.append(node) + self.__signatures.append(node) def save(self, path): from .signature import save_signatures @@ -359,6 +423,35 @@ def load(cls, location): lidx = LinearIndex(si, filename=location) return lidx + def find(self, search_fn, query, **kwargs): + """Use search_fn to find matching signatures in the index. + + search_fn follows the protocol in JaccardSearch objects. + + Returns a list. + """ + self._init_inner() + + size = ffi.new("uintptr_t *") + results_ptr = self._methodcall( + lib.linearindex_find, + search_fn._as_rust(), + query._get_objptr(), + size + ) + + size = size[0] + if size == 0: + return [] + + results = [] + for i in range(size): + match = SearchResult._from_objptr(results_ptr[i]) + results.append(IndexSearchResult(match.score, match.signature, self.filename)) + + for sr in results: + yield sr + def select(self, **kwargs): """Return new LinearIndex containing only signatures that match req's. @@ -368,13 +461,33 @@ def select(self, **kwargs): kw = { k : v for (k, v) in kwargs.items() if v } siglist = [] - for ss in self._signatures: + for ss in self.signatures(): if select_signature(ss, **kwargs): siglist.append(ss) return LinearIndex(siglist, self.location) +class SearchResult(RustObject): + __dealloc_func__ = lib.searchresult_free + + @property + def score(self): + return self._methodcall(lib.searchresult_score) + + @property + def signature(self): + sig_ptr = self._methodcall(lib.searchresult_signature) + return sourmash.SourmashSignature._from_objptr(sig_ptr) + + @property + def filename(self): + result = decode_str(self._methodcall(lib.searchresult_filename)) + if result == "": + return None + return result + + class LazyLinearIndex(Index): """An Index for lazy linear search of another database. diff --git a/src/sourmash/search.py b/src/sourmash/search.py index 93d77920ce..fa763024c8 100644 --- a/src/sourmash/search.py +++ b/src/sourmash/search.py @@ -9,6 +9,8 @@ from .logging import notify, error from .signature import SourmashSignature from .minhash import _get_max_hash_for_scaled +from .utils import rustcall +from ._lowlevel import ffi, lib class SearchType(Enum): @@ -98,6 +100,7 @@ def __init__(self, search_type, threshold=None): require_scaled = True self.score_fn = score_fn self.require_scaled = require_scaled + self.search_type = search_type if threshold is None: threshold = 0 @@ -150,14 +153,48 @@ def score_max_containment(self, query_size, shared_size, subject_size, return 0 return shared_size / min_denom + def _as_rust(self): + """ + Return a compatible Rust search function. + + The Rust function duplicates the implementation of this class, since + there is no good way to call back into Python code without involving a + lot of machinery. + """ + + return rustcall( + lib.searchfn_new, + self.search_type.value, + self.threshold, + False + ) + class JaccardSearchBestOnly(JaccardSearch): "A subclass of JaccardSearch that implements best-only." + def collect(self, score, match): "Raise the threshold to the best match found so far." self.threshold = max(self.threshold, score) return True + def _as_rust(self): + """ + Return a compatible Rust search function. + + The Rust function duplicates the implementation of this class, since + there is no good way to call back into Python code without involving a + lot of machinery. + """ + + return rustcall( + lib.searchfn_new, + self.search_type.value, + self.threshold, + True + ) + + # generic SearchResult tuple. SearchResult = namedtuple('SearchResult',