Scanning every note for every query is a useful first implementation, but it repeats work. An inverted index turns the direction around: look up a term, then visit the documents known to contain it. The index is derived data. Its IDs must still refer to the validated snapshot, and its terms must come from the shared tokenizer.
We keep two maps. lengths records the total number of body tokens for every document. postings records each term’s positive occurrence count in each containing document. Titles remain display metadata. A note titled red with an empty body has length zero and contributes no red posting.
A posting count is not a Boolean membership flag. When a token repeats, increment that document’s count. Also keep token length separate from distinct vocabulary size. In red red blue, length is three, the vocabulary has two members, and red occurs twice. The ranking lesson uses all three ideas for different purposes.
from collections import Counter
tokens = 'red red blue'.split()
print(len(tokens), dict(Counter(tokens)))
print(len(set(tokens)))An empty document still belongs in lengths. Later N means all documents in the corpus, whereas a term’s document frequency counts only the IDs in its posting map. Removing empty records silently changes rarity weights. The corpus may be empty too; in that case both maps are empty and ranking has nothing to visit.
Build fresh maps on every call. Sorting outer term keys and inner ID keys makes exported JSON and fixtures easy to compare. Sorting is a presentation and reproducibility choice; it is separate from the relevance ranking of results.
lengths = {'a': 0, 'b': 3}
red_posting = {'b': 2}
print('N', len(lengths))
print('df', len(red_posting))
print('tf', red_posting['b'])The build supplies validation and tokenization so you can focus on count ownership. Check repeated terms in one document, the same term in several documents, an empty body, an empty corpus and a second independent call. For larger collections this in-memory representation is only a starting point: disk layouts, compression and incremental updates need additional contracts and measurements.