Posts
Search
Search finds people, groups, events, posts and shared feeds by what they are called or what they say. This page is about the order results come in, and how an operator tunes it.
Its route is in the API reference: search-service.
Two orders
The search page offers two: Most relevant, the default, and Most recent, newest first. Most recent is only that. The rest of this page is about Most relevant, which also orders a hashtag's and a location's posts (hashtags.md), without the words.
Most relevant
Results are ordered by three things, one after the other. The second only decides between results the first puts level, and the third between those the second puts level.
-
Where the words are.
Where Points by default The handle exactly ( rastin.example,ana), with or without the@typed before it100 The name exactly 90 The name begins with them 70 A word in the name begins with them ("Jericho Beach") 60 Somewhere inside the name, or inside the handle ( decafcoffeeclub)50 A hashtag on the post, or, on a photo or a video, the name or the handle of somebody in it, their tag approved 40 A whole word in the body 20 Only inside a word in the body ("coffeehouse") 10 The first that holds is the one counted. Only the order of the points matters: a profile called Coffee comes before a post that mentions coffee however much the post is liked.
-
Its standing: how useful people have found it, and whether the searcher follows it.
standing = scale × ln(1 + like × likes + save × saves + reply × (replies, reposts and quotes) + follow × followers + cite × citers) + following, when the searcher follows it or the profile it is on + pictured_with_followed, when somebody the searcher follows is in it, or it is a person in public photos or videos with themPictured with whom the searcher follows (roadmap row 104): a photo or a video with somebody they follow in it, or a person in public photos or videos with them, tagged or as the one who posted it. Approved tags only. A person's own "following" step moves it with following, and "Profiles I follow first" puts it after what they follow themselves.
The logarithm keeps one post liked a thousand times from burying everything: it comes first, not a hundred times first. Each like, save, reply or follow counts by when it happened (below).
Citers are the people who show it within an article or a topic of their own, each once however many of their posts do, and never its own author; a profile's are those of its posts. Showing a profile is naming it, a mention, and not a citation:
nodes.citation_count, kept in step by the graph (repositories/citations.go). Only a published post anybody may read counts as citing. A citation counts as it stands, not halved with time: citing is deliberate and lasting, and useful knowledge stays useful.citestarts at 3, above a like (1) and a save (2), since citing takes effort; it is a starting value, to be tuned once there are citations to look at. -
Its id, the newest first, so that results level on everything keep their places from one page to the next.
With a distance chosen, the nearest come first among results level on the first two.
Old and useful counts as much as new
Anahita is for building knowledge as well as for news: a school's notes, a
library's guides, a research group's findings stay useful for years. So
nothing is pushed down for being old. What may fade is interest: each
like, save, reply or follow is counted by when it happened, halved every
half_life_days.
- A guide written years ago that people saved this month stands as high as a post from this week that was saved as often.
- A post nobody has touched in a long while weighs only its old interest.
half_life_days: 0lets nothing fade: interest counts the same however old.
By default it is 365 days. A newsroom may want a month; a library, a school or a research group a year or more, or 0.
Tuning it
Every number above is the operator's, in
graph-grpc-defaults/search_ranking.json:
{
"match": {
"handle": 100, "name": 90, "name_starts": 70, "word_starts": 60,
"name_contains": 50, "hashtag": 40, "body_word": 20, "body_contains": 10
},
"following": 15,
"pictured_with_followed": 8,
"useful": { "scale": 5, "like": 1, "save": 2, "reply": 2, "follow": 1, "cite": 3 },
"half_life_days": 365
}
- graph-grpc-service reads it from
SEARCH_RANKING_PATH, which is/app/defaults/search_ranking.jsonin the image. docker-compose mounts the working tree's copy there. On Kubernetes, mount a ConfigMap over that path to change it. - It is read again when it changes, without a restart.
- What it leaves out keeps its default, so a file with only
{"half_life_days": 30}changes that and nothing else. - A file that cannot be read, or has a number below 0 or above 1000, is not taken: the default weights are used, and the service says so in its log.
Each person's choices
Two, off as they come, sent with a search as followed_first=true and
recent_first=true and only with "Most relevant":
- Profiles I follow first: what the searcher follows, or what is on a profile they follow, before everything else, however it matches. Nothing changes for a visitor, who follows nobody.
- Newer results first: among results that match alike, the newest first, and only then by standing. The match still comes first: a profile called Coffee is still above a post that mentions coffee.
The web app keeps them in the searcher's browser, per person.
Their own weights
Each person who is signed in can also change how much each sign counts for them, as they can for their suggestions (the "Adjust" chip by "Most relevant"). There are six: likes, saves, replies, followers, citations, and who or what they follow (the people, groups and events, and what is posted on them). Each is set to a step that multiplies the operator's weight:
| Step | Multiplies by |
|---|---|
| Off | 0 |
| Less | 0.5 |
| As is | 1 |
| More | 1.5 |
| Much more | 2 |
So with the default file, a citation set to Much more counts 6 for that person, and a like set to Off counts nothing. The match always comes first; the steps only change the order among results that match alike.
GET /search/preferencesanswers with the person's steps, which may be set (adjustable) and to what (options). A name not instepsis As is.PUT /search/preferenceswith{"steps": {"cite": 2, "like": 0}}replaces them all. A name that may not be set, or a value that is not a step, is refused with 422invalid_weight, and nothing changes.- They are kept on the server, in
search_preferences, so they follow the person to any device. Only steps other than As is are stored, and the row is removed when the profile is purged. - The names and steps are
constants.SearchAdjustableandconstants.SearchStepsin anahita-libs. The graph applies them withranking.Weights.Adjusted.
What is found at all
The order does not change what is found. A result has the words in its
name, its handle (matched without a typed @), or its body, or is a photo
or a video with somebody of that name or handle in it, and is one the
searcher may see; see Permissions.
Where it looks (scope on GET /search/), each a tab in the web app:
scope |
Finds |
|---|---|
all (the default) |
Everything below |
posts |
Every kind of post: notes, articles, topics, photos and videos |
media |
Photos and videos alone, as other networks mean media (the user, 2026-10-20) |
actors |
Profiles: people, groups, events and websites, here and elsewhere |
feeds |
Custom feeds people have shared |
Until 2026-10-20 media was every kind of post; it is posts now.
What may be searched for (search_query, anahita-libs/validators):
letters, marks and numbers in any language ("Montréal", "Zürich", "東京"),
spaces, and what people type in a name, a handle or an address: - _ .
, + # @ " ' ’ : / ? & = ~ ! ( ). Up to 100
characters. It refused everything but unaccented Latin letters until
2026-10-19. A search among names (name_search_query, the lists of a
profile's followers) takes the same letters with - _ . + " ' ’.
None of it is written into SQL: the words are bound parameters, LIKE's
wildcards are taken out, and a regular expression made of them is quoted.
Where the code is
graph-grpc-service/data/repositories/search.go: what is found (wordsMatch), the two scores (relevance), and the order, ranking the page's ids before reading their rows (browseByRelevance).graph-grpc-service/ranking: the weights, the file, reading it again, and each person's steps (Adjusted).graph-grpc-service/data/repositories/search_preferences.goandsearch-service/cmd/api/handlers/preferences.go: each person's steps, kept and given.graph-grpc-service/data/repositories/search_ranking_integration_test.go: the order checked against MySQL, with searches as people make them.
What is not built
- An index of words. Search matches with
LIKE '%…%', which reads every row and knows nothing of words or languages. MySQL'sFULLTEXTindex with thengramparser was tried and put off: it missed posts thatLIKEfinds (two-letter stopwords, text indexed before the index existed, Japanese). Search is fast enough without it at today's sizes; a search engine beside the database is the likelier answer when it is not. - Ranking by the citers' own standing, as Google ranks the web, where
a citation from what is itself much cited counts for more. Citations
count today each as one (above); weighing them by who cites is for when
there are enough to tune it against. See the web app's
docs/plans/done/search-improvements.md.
From anahitasocial/anahita-services, 2026-10-27.
Written by Claude, reviewed by Rastin Mehr · About this