2019-10-02 17:34:32 +02:00
|
|
|
|
use std::collections::{BTreeMap, HashMap};
|
|
|
|
|
use std::convert::TryFrom;
|
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
use crate::{DocIndex, DocumentId};
|
2019-10-02 17:34:32 +02:00
|
|
|
|
use deunicode::deunicode_with_tofu;
|
2020-01-10 18:20:30 +01:00
|
|
|
|
use meilisearch_schema::IndexedPos;
|
2019-11-26 11:06:55 +01:00
|
|
|
|
use meilisearch_tokenizer::{is_cjk, SeqTokenizer, Token, Tokenizer};
|
2019-10-02 17:34:32 +02:00
|
|
|
|
use sdset::SetBuf;
|
|
|
|
|
|
2019-11-10 17:41:32 +01:00
|
|
|
|
const WORD_LENGTH_LIMIT: usize = 80;
|
|
|
|
|
|
2019-10-02 17:34:32 +02:00
|
|
|
|
type Word = Vec<u8>; // TODO make it be a SmallVec
|
|
|
|
|
|
|
|
|
|
pub struct RawIndexer {
|
|
|
|
|
word_limit: usize, // the maximum number of indexed words
|
2019-10-29 15:53:45 +01:00
|
|
|
|
stop_words: fst::Set,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
words_doc_indexes: BTreeMap<Word, Vec<DocIndex>>,
|
|
|
|
|
docs_words: HashMap<DocumentId, Vec<Word>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Indexed {
|
|
|
|
|
pub words_doc_indexes: BTreeMap<Word, SetBuf<DocIndex>>,
|
|
|
|
|
pub docs_words: HashMap<DocumentId, fst::Set>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RawIndexer {
|
2019-10-29 15:53:45 +01:00
|
|
|
|
pub fn new(stop_words: fst::Set) -> RawIndexer {
|
|
|
|
|
RawIndexer::with_word_limit(stop_words, 1000)
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-29 15:53:45 +01:00
|
|
|
|
pub fn with_word_limit(stop_words: fst::Set, limit: usize) -> RawIndexer {
|
2019-10-02 17:34:32 +02:00
|
|
|
|
RawIndexer {
|
|
|
|
|
word_limit: limit,
|
2019-10-29 15:53:45 +01:00
|
|
|
|
stop_words,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
words_doc_indexes: BTreeMap::new(),
|
|
|
|
|
docs_words: HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-10 18:20:30 +01:00
|
|
|
|
pub fn index_text(&mut self, id: DocumentId, indexed_pos: IndexedPos, text: &str) -> usize {
|
2019-10-14 13:56:52 +02:00
|
|
|
|
let mut number_of_words = 0;
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
2019-11-04 16:09:32 +01:00
|
|
|
|
for token in Tokenizer::new(text) {
|
2019-10-02 17:34:32 +02:00
|
|
|
|
let must_continue = index_token(
|
|
|
|
|
token,
|
|
|
|
|
id,
|
2020-01-10 18:20:30 +01:00
|
|
|
|
indexed_pos,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
self.word_limit,
|
2019-10-29 15:53:45 +01:00
|
|
|
|
&self.stop_words,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
&mut self.words_doc_indexes,
|
|
|
|
|
&mut self.docs_words,
|
|
|
|
|
);
|
|
|
|
|
|
2019-11-04 16:09:32 +01:00
|
|
|
|
number_of_words += 1;
|
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
if !must_continue {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-04 16:09:32 +01:00
|
|
|
|
number_of_words
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
2020-01-10 18:20:30 +01:00
|
|
|
|
pub fn index_text_seq<'a, I>(&mut self, id: DocumentId, indexed_pos: IndexedPos, iter: I)
|
2019-11-04 16:09:32 +01:00
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = &'a str>,
|
|
|
|
|
{
|
|
|
|
|
let iter = iter.into_iter();
|
2019-10-02 17:34:32 +02:00
|
|
|
|
for token in SeqTokenizer::new(iter) {
|
|
|
|
|
let must_continue = index_token(
|
|
|
|
|
token,
|
|
|
|
|
id,
|
2020-01-10 18:20:30 +01:00
|
|
|
|
indexed_pos,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
self.word_limit,
|
2019-10-29 15:53:45 +01:00
|
|
|
|
&self.stop_words,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
&mut self.words_doc_indexes,
|
|
|
|
|
&mut self.docs_words,
|
|
|
|
|
);
|
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
if !must_continue {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build(self) -> Indexed {
|
2019-10-18 13:05:28 +02:00
|
|
|
|
let words_doc_indexes = self
|
|
|
|
|
.words_doc_indexes
|
2019-10-02 17:34:32 +02:00
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(word, indexes)| (word, SetBuf::from_dirty(indexes)))
|
|
|
|
|
.collect();
|
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
let docs_words = self
|
|
|
|
|
.docs_words
|
2019-10-02 17:34:32 +02:00
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(id, mut words)| {
|
|
|
|
|
words.sort_unstable();
|
|
|
|
|
words.dedup();
|
|
|
|
|
(id, fst::Set::from_iter(words).unwrap())
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
Indexed {
|
|
|
|
|
words_doc_indexes,
|
|
|
|
|
docs_words,
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn index_token(
|
|
|
|
|
token: Token,
|
|
|
|
|
id: DocumentId,
|
2020-01-10 18:20:30 +01:00
|
|
|
|
indexed_pos: IndexedPos,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
word_limit: usize,
|
2019-10-29 15:53:45 +01:00
|
|
|
|
stop_words: &fst::Set,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
words_doc_indexes: &mut BTreeMap<Word, Vec<DocIndex>>,
|
|
|
|
|
docs_words: &mut HashMap<DocumentId, Vec<Word>>,
|
2019-10-18 13:05:28 +02:00
|
|
|
|
) -> bool {
|
|
|
|
|
if token.word_index >= word_limit {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
2019-11-04 16:09:32 +01:00
|
|
|
|
let lower = token.word.to_lowercase();
|
|
|
|
|
let token = Token {
|
|
|
|
|
word: &lower,
|
|
|
|
|
..token
|
|
|
|
|
};
|
|
|
|
|
|
2019-10-29 16:04:48 +01:00
|
|
|
|
if !stop_words.contains(&token.word) {
|
2020-01-10 18:20:30 +01:00
|
|
|
|
match token_to_docindex(id, indexed_pos, token) {
|
2019-10-29 16:04:48 +01:00
|
|
|
|
Some(docindex) => {
|
|
|
|
|
let word = Vec::from(token.word);
|
2019-11-10 17:41:32 +01:00
|
|
|
|
|
|
|
|
|
if word.len() <= WORD_LENGTH_LIMIT {
|
|
|
|
|
words_doc_indexes
|
|
|
|
|
.entry(word.clone())
|
|
|
|
|
.or_insert_with(Vec::new)
|
|
|
|
|
.push(docindex);
|
|
|
|
|
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
|
|
|
|
|
|
|
|
|
if !lower.contains(is_cjk) {
|
|
|
|
|
let unidecoded = deunicode_with_tofu(&lower, "");
|
|
|
|
|
if unidecoded != lower && !unidecoded.is_empty() {
|
|
|
|
|
let word = Vec::from(unidecoded);
|
|
|
|
|
if word.len() <= WORD_LENGTH_LIMIT {
|
|
|
|
|
words_doc_indexes
|
|
|
|
|
.entry(word.clone())
|
|
|
|
|
.or_insert_with(Vec::new)
|
|
|
|
|
.push(docindex);
|
|
|
|
|
docs_words.entry(id).or_insert_with(Vec::new).push(word);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-04 16:09:32 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-11-05 16:40:34 +01:00
|
|
|
|
None => return false,
|
2019-11-04 16:09:32 +01:00
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-10 18:20:30 +01:00
|
|
|
|
fn token_to_docindex(id: DocumentId, indexed_pos: IndexedPos, token: Token) -> Option<DocIndex> {
|
2019-10-02 17:34:32 +02:00
|
|
|
|
let word_index = u16::try_from(token.word_index).ok()?;
|
|
|
|
|
let char_index = u16::try_from(token.char_index).ok()?;
|
|
|
|
|
let char_length = u16::try_from(token.word.chars().count()).ok()?;
|
|
|
|
|
|
|
|
|
|
let docindex = DocIndex {
|
|
|
|
|
document_id: id,
|
2020-01-10 18:20:30 +01:00
|
|
|
|
attribute: indexed_pos.0,
|
2019-10-02 17:34:32 +02:00
|
|
|
|
word_index,
|
|
|
|
|
char_index,
|
|
|
|
|
char_length,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(docindex)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2020-01-13 19:10:58 +01:00
|
|
|
|
use meilisearch_schema::IndexedPos;
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn strange_apostrophe() {
|
2019-10-29 15:53:45 +01:00
|
|
|
|
let mut indexer = RawIndexer::new(fst::Set::default());
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
|
|
|
|
let docid = DocumentId(0);
|
2020-01-13 19:10:58 +01:00
|
|
|
|
let indexed_pos = IndexedPos(0);
|
2019-10-02 17:34:32 +02:00
|
|
|
|
let text = "Zut, l’aspirateur, j’ai oublié de l’éteindre !";
|
2020-01-13 19:10:58 +01:00
|
|
|
|
indexer.index_text(docid, indexed_pos, text);
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
let Indexed {
|
|
|
|
|
words_doc_indexes, ..
|
|
|
|
|
} = indexer.build();
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
|
|
|
|
assert!(words_doc_indexes.get(&b"l"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"aspirateur"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"ai"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"eteindre"[..]).is_some());
|
2019-10-18 13:05:28 +02:00
|
|
|
|
assert!(words_doc_indexes
|
2019-11-04 16:10:31 +01:00
|
|
|
|
.get(&"éteindre".to_owned().into_bytes())
|
2019-10-18 13:05:28 +02:00
|
|
|
|
.is_some());
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn strange_apostrophe_in_sequence() {
|
2019-10-29 15:53:45 +01:00
|
|
|
|
let mut indexer = RawIndexer::new(fst::Set::default());
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
|
|
|
|
let docid = DocumentId(0);
|
2020-01-13 19:10:58 +01:00
|
|
|
|
let indexed_pos = IndexedPos(0);
|
2019-10-02 17:34:32 +02:00
|
|
|
|
let text = vec!["Zut, l’aspirateur, j’ai oublié de l’éteindre !"];
|
2020-01-13 19:10:58 +01:00
|
|
|
|
indexer.index_text_seq(docid, indexed_pos, text);
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
2019-10-18 13:05:28 +02:00
|
|
|
|
let Indexed {
|
|
|
|
|
words_doc_indexes, ..
|
|
|
|
|
} = indexer.build();
|
2019-10-02 17:34:32 +02:00
|
|
|
|
|
|
|
|
|
assert!(words_doc_indexes.get(&b"l"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"aspirateur"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"ai"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"eteindre"[..]).is_some());
|
2019-10-18 13:05:28 +02:00
|
|
|
|
assert!(words_doc_indexes
|
2019-11-04 16:10:31 +01:00
|
|
|
|
.get(&"éteindre".to_owned().into_bytes())
|
2019-10-18 13:05:28 +02:00
|
|
|
|
.is_some());
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|
2019-10-29 16:04:48 +01:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn basic_stop_words() {
|
|
|
|
|
let stop_words = sdset::SetBuf::from_dirty(vec!["l", "j", "ai", "de"]);
|
|
|
|
|
let stop_words = fst::Set::from_iter(stop_words).unwrap();
|
|
|
|
|
|
|
|
|
|
let mut indexer = RawIndexer::new(stop_words);
|
|
|
|
|
|
|
|
|
|
let docid = DocumentId(0);
|
2020-01-13 19:10:58 +01:00
|
|
|
|
let indexed_pos = IndexedPos(0);
|
2019-10-29 16:04:48 +01:00
|
|
|
|
let text = "Zut, l’aspirateur, j’ai oublié de l’éteindre !";
|
2020-01-13 19:10:58 +01:00
|
|
|
|
indexer.index_text(docid, indexed_pos, text);
|
2019-10-29 16:04:48 +01:00
|
|
|
|
|
|
|
|
|
let Indexed {
|
|
|
|
|
words_doc_indexes, ..
|
|
|
|
|
} = indexer.build();
|
|
|
|
|
|
|
|
|
|
assert!(words_doc_indexes.get(&b"l"[..]).is_none());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"aspirateur"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"j"[..]).is_none());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"ai"[..]).is_none());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"de"[..]).is_none());
|
|
|
|
|
assert!(words_doc_indexes.get(&b"eteindre"[..]).is_some());
|
|
|
|
|
assert!(words_doc_indexes
|
2019-11-04 16:10:31 +01:00
|
|
|
|
.get(&"éteindre".to_owned().into_bytes())
|
2019-10-29 16:04:48 +01:00
|
|
|
|
.is_some());
|
|
|
|
|
}
|
2019-11-04 16:58:02 +01:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn no_empty_unidecode() {
|
|
|
|
|
let mut indexer = RawIndexer::new(fst::Set::default());
|
|
|
|
|
|
|
|
|
|
let docid = DocumentId(0);
|
2020-01-13 19:10:58 +01:00
|
|
|
|
let indexed_pos = IndexedPos(0);
|
2019-11-04 16:58:02 +01:00
|
|
|
|
let text = "🇯🇵";
|
2020-01-13 19:10:58 +01:00
|
|
|
|
indexer.index_text(docid, indexed_pos, text);
|
2019-11-04 16:58:02 +01:00
|
|
|
|
|
|
|
|
|
let Indexed {
|
|
|
|
|
words_doc_indexes, ..
|
|
|
|
|
} = indexer.build();
|
|
|
|
|
|
|
|
|
|
assert!(words_doc_indexes
|
|
|
|
|
.get(&"🇯🇵".to_owned().into_bytes())
|
|
|
|
|
.is_some());
|
|
|
|
|
}
|
2019-10-02 17:34:32 +02:00
|
|
|
|
}
|