@@ -10,18 +10,72 @@ use std::sync::Arc;
1010
1111create_exception ! ( simstring_rust, SearchError , pyo3:: exceptions:: PyValueError ) ;
1212
13+ #[ derive( Clone ) ]
14+ struct CustomExtractorInner {
15+ extractor : Py < PyAny > ,
16+ }
17+
18+ unsafe impl Send for CustomExtractorInner { }
19+ unsafe impl Sync for CustomExtractorInner { }
20+
21+ impl CustomExtractorInner {
22+ fn new ( extractor : Py < PyAny > ) -> Self {
23+ Self { extractor }
24+ }
25+
26+ fn collect_raw_features ( & self , text : & str ) -> PyResult < Vec < String > > {
27+ Python :: with_gil ( |py| {
28+ let extractor = self . extractor . bind ( py) ;
29+ let result = extractor. call_method1 ( "apply" , ( text, ) ) ?;
30+ let iter = result. iter ( ) ?;
31+
32+ let mut features = Vec :: new ( ) ;
33+ for item in iter {
34+ let item = item?;
35+ features. push ( item. extract :: < String > ( ) ?) ;
36+ }
37+
38+ Ok ( features)
39+ } )
40+ }
41+
42+ fn features ( & self , text : & str , interner : & mut lasso:: Rodeo ) -> PyResult < Vec < lasso:: Spur > > {
43+ let raw = self . collect_raw_features ( text) ?;
44+ Ok ( super :: append_feature_counts ( interner, raw) )
45+ }
46+
47+ fn apply ( & self , text : & str ) -> PyResult < Vec < String > > {
48+ let raw = self . collect_raw_features ( text) ?;
49+ let mut interner = lasso:: Rodeo :: default ( ) ;
50+ let spurs = super :: append_feature_counts ( & mut interner, raw) ;
51+
52+ Ok ( spurs
53+ . into_iter ( )
54+ . map ( |spur| interner. resolve ( & spur) . to_string ( ) )
55+ . collect ( ) )
56+ }
57+ }
58+
1359// Wrapper for FeatureExtractor trait as I can't find any direct translation.
1460#[ derive( Clone ) ]
1561enum PyFeatureExtractor {
1662 Character ( CharacterNgrams ) ,
1763 Word ( WordNgrams ) ,
64+ Custom ( CustomExtractorInner ) ,
1865}
1966
2067impl FeatureExtractor for PyFeatureExtractor {
2168 fn features ( & self , text : & str , interner : & mut lasso:: Rodeo ) -> Vec < lasso:: Spur > {
2269 match self {
2370 PyFeatureExtractor :: Character ( e) => e. features ( text, interner) ,
2471 PyFeatureExtractor :: Word ( e) => e. features ( text, interner) ,
72+ PyFeatureExtractor :: Custom ( e) => match e. features ( text, interner) {
73+ Ok ( features) => features,
74+ Err ( err) => {
75+ Python :: with_gil ( |py| err. print ( py) ) ;
76+ panic ! ( "Custom extractor apply() raised an exception" ) ;
77+ }
78+ } ,
2579 }
2680 }
2781}
@@ -70,6 +124,28 @@ impl PyWordNgrams {
70124 }
71125}
72126
127+ #[ pyclass( name = "CustomExtractor" ) ]
128+ #[ derive( Clone ) ]
129+ struct PyCustomExtractor ( CustomExtractorInner ) ;
130+
131+ #[ pymethods]
132+ impl PyCustomExtractor {
133+ #[ new]
134+ fn new ( extractor : & Bound < ' _ , PyAny > ) -> PyResult < Self > {
135+ if !extractor. hasattr ( "apply" ) ? {
136+ return Err ( pyo3:: exceptions:: PyTypeError :: new_err (
137+ "Custom extractor must provide an apply(text: str) -> Iterable[str] method" ,
138+ ) ) ;
139+ }
140+
141+ Ok ( Self ( CustomExtractorInner :: new ( extractor. unbind ( ) ) ) )
142+ }
143+
144+ fn apply ( & self , text : & str ) -> PyResult < Vec < String > > {
145+ self . 0 . apply ( text)
146+ }
147+ }
148+
73149// Wrapper for Measure trait
74150#[ derive( Clone , Copy ) ]
75151enum PyMeasure {
@@ -193,9 +269,11 @@ impl PyHashDb {
193269 PyFeatureExtractor :: Character ( char_ngram. 0 . clone ( ) )
194270 } else if let Ok ( word_ngram) = extractor. extract :: < PyRef < PyWordNgrams > > ( ) {
195271 PyFeatureExtractor :: Word ( word_ngram. 0 . clone ( ) )
272+ } else if let Ok ( custom) = extractor. extract :: < PyRef < PyCustomExtractor > > ( ) {
273+ PyFeatureExtractor :: Custom ( custom. 0 . clone ( ) )
196274 } else {
197275 return Err ( pyo3:: exceptions:: PyTypeError :: new_err (
198- "Extractor must be CharacterNgrams or WordNgrams " ,
276+ "Extractor must be CharacterNgrams, WordNgrams, or CustomExtractor " ,
199277 ) ) ;
200278 } ;
201279
@@ -300,6 +378,7 @@ fn simstring_rust(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
300378 let extractors_module = PyModule :: new ( py, "extractors" ) ?;
301379 extractors_module. add_class :: < PyCharacterNgrams > ( ) ?;
302380 extractors_module. add_class :: < PyWordNgrams > ( ) ?;
381+ extractors_module. add_class :: < PyCustomExtractor > ( ) ?;
303382 m. add_submodule ( & extractors_module) ?;
304383
305384 // Measures submodule
0 commit comments