Every knob below except SEARCH_LIMIT is an environment variable on the API, so changing one is a restart, not a deploy — and never a client update. SEARCH_LIMIT is a constant in app/config.py, deliberately: how much to return is a retrieval decision, not an operator setting.

Ranking weights

Only the ratio matters. Some reference points for the vote term at the default weight:
Raising SEARCH_W_VOTES much above ~0.3 lets a well-voted note beat a clearly more relevant one, and the effect compounds — a note that ranks high gets used, gets upvoted, and ranks higher still. If you want popularity to matter more, the durable fix is usually better note titles, not a heavier vote term.

Embedding model and dimension

Common widths: text-embedding-3-small = 1536, nomic-embed-text = 768, mxbai-embed-large = 1024.
Changing the model is not a config change alone. A new width requires retyping posts.embedding, rebuilding the HNSW index, and re-embedding every note — vectors from two different models are not comparable, so a partial migration silently corrupts ranking. The procedure is in Embeddings.

Timeouts

Raise the query timeout on a self-hosted local model if you are seeing 503s under load; raise the post timeout if you see embedding IS NULL accumulating.

How to tell whether a change helped

Every search is logged with its result set and each hit’s rank, so ranking changes are observable rather than felt. The three numbers worth tracking before and after any tuning change:

Zero-result rate

Share of searches returning nothing. Rising means recall is getting worse or the corpus is too thin.

Vote-through rate

Share of searches followed by a vote on one of their results, within the session. The closest proxy available for “the top results were useful”.

Rank of the voted hit

When a result is voted, where did it sit? Drifting down means ranking is degrading even while recall looks fine.
SQL for all three is in Operations. Move one knob at a time and give it enough traffic to mean something.
The similarity value is not persisted anywhere. search_results.score_at is the note’s vote score at query time, not its relevance to the query — the column name reads both ways and only one is true. So you can measure rank, zero-result rate, and vote-through, but you cannot see the similarity distribution behind a result set, and you cannot tell a strong top hit from a weak one that merely had no competition.That matters most if you are tuning SEARCH_W_VOTES: you will see ordering move without being able to see how far apart the candidates were to begin with.